我将详细讲解 React 中两个核心状态管理 Hook:useState 和 useReducer,从基础到高级用法。
一、useState 基础用法
1.1 基本概念
useState 是 React 中最基础的状态管理 Hook,用于在函数组件中添加状态。
jsx
import React, { useState } from 'react';
function Counter() {
// 基本用法:state 和 setState 函数
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
<button onClick={() => setCount(count - 1)}>减少</button>
</div>
);
}
1.2 初始化方式
jsx
// 方式1:直接值初始化
const [name, setName] = useState('张三');
// 方式2:函数初始化(惰性初始化,性能优化)
const [todos, setTodos] = useState(() => {
const initialTodos = JSON.parse(localStorage.getItem('todos')) || [];
return initialTodos;
});
// 方式3:对象状态
const [user, setUser] = useState({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
});
1.3 更新状态的不同方式
jsx
function UserProfile() {
const [user, setUser] = useState({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
});
const [count, setCount] = useState(0);
// 1. 直接设置新值
const updateName = () => {
setUser({ ...user, name: '李四' });
};
// 2. 使用函数更新(基于前一个状态)
const incrementTwice = () => {
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1); // 确保基于最新状态
};
// 3. 部分更新对象
const updateAge = (newAge) => {
setUser(prevUser => ({
...prevUser,
age: newAge
}));
};
// 4. 重置状态
const resetUser = () => {
setUser({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
});
};
return (
<div>
<p>姓名: {user.name}</p>
<p>年龄: {user.age}</p>
<p>计数: {count}</p>
<button onClick={updateName}>改名</button>
<button onClick={() => updateAge(30)}>改年龄</button>
<button onClick={incrementTwice}>增加两次</button>
<button onClick={resetUser}>重置</button>
</div>
);
}
1.4 复杂状态管理示例
jsx
function TodoList() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
// 添加 todo
const addTodo = () => {
if (!inputValue.trim()) return;
const newTodo = {
id: Date.now(),
text: inputValue,
completed: false
};
setTodos(prevTodos => [...prevTodos, newTodo]);
setInputValue('');
};
// 切换完成状态
const toggleTodo = (id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
// 删除 todo
const deleteTodo = (id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};
// 清空所有
const clearAll = () => {
setTodos([]);
};
return (
<div>
<h2>Todo List</h2>
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="输入待办事项"
/>
<button onClick={addTodo}>添加</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>删除</button>
</li>
))}
</ul>
<div>
<p>总任务数: {todos.length}</p>
<p>已完成: {todos.filter(t => t.completed).length}</p>
<button onClick={clearAll}>清空所有</button>
</div>
</div>
);
}
二、useReducer 基础用法
2.1 基本概念
useReducer 是更复杂的状态管理方案,适合状态逻辑复杂或包含多个子值的场景。
jsx
import React, { useReducer } from 'react';
// 1. 定义 reducer 函数
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
case 'SET':
return { count: action.payload };
default:
return state;
}
}
function CounterWithReducer() {
// 2. 使用 useReducer
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>增加</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>减少</button>
<button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
<button onClick={() => dispatch({ type: 'SET', payload: 10 })}>
设置为10
</button>
</div>
);
}
2.2 Reducer 的三种写法
jsx
// 写法1:switch 语句(最常用)
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, action.payload]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'SET_FILTER':
return {
...state,
filter: action.payload
};
default:
return state;
}
}
// 写法2:对象查找(适合简单场景)
function calculatorReducer(state, action) {
const handlers = {
ADD: () => ({ result: state.result + action.payload }),
SUBTRACT: () => ({ result: state.result - action.payload }),
MULTIPLY: () => ({ result: state.result * action.payload }),
DIVIDE: () => ({ result: state.result / action.payload }),
RESET: () => ({ result: 0 })
};
return handlers[action.type] ? handlers[action.type]() : state;
}
// 写法3:函数工厂模式(可扩展性强)
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action);
}
return state;
};
}
// 使用工厂模式
const userReducer = createReducer(
{ name: '', age: 0, loading: false },
{
SET_NAME: (state, action) => ({ ...state, name: action.payload }),
SET_AGE: (state, action) => ({ ...state, age: action.payload }),
SET_LOADING: (state, action) => ({ ...state, loading: action.payload })
}
);
2.3 复杂状态管理示例
jsx
const initialState = {
cartItems: [],
totalPrice: 0,
totalQuantity: 0,
discount: 0,
isLoading: false,
error: null
};
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
const existingItem = state.cartItems.find(
item => item.id === action.payload.id
);
let updatedItems;
if (existingItem) {
updatedItems = state.cartItems.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
updatedItems = [...state.cartItems, { ...action.payload, quantity: 1 }];
}
return {
...state,
cartItems: updatedItems,
totalQuantity: state.totalQuantity + 1,
totalPrice: state.totalPrice + action.payload.price
};
case 'REMOVE_ITEM':
const itemToRemove = state.cartItems.find(
item => item.id === action.payload
);
if (!itemToRemove) return state;
const filteredItems = state.cartItems.filter(
item => item.id !== action.payload
);
return {
...state,
cartItems: filteredItems,
totalQuantity: state.totalQuantity - itemToRemove.quantity,
totalPrice: state.totalPrice - (itemToRemove.price * itemToRemove.quantity)
};
case 'UPDATE_QUANTITY':
const { id, quantity } = action.payload;
const itemToUpdate = state.cartItems.find(item => item.id === id);
if (!itemToUpdate || quantity < 1) return state;
const quantityDiff = quantity - itemToUpdate.quantity;
const updatedCartItems = state.cartItems.map(item =>
item.id === id ? { ...item, quantity } : item
);
return {
...state,
cartItems: updatedCartItems,
totalQuantity: state.totalQuantity + quantityDiff,
totalPrice: state.totalPrice + (itemToUpdate.price * quantityDiff)
};
case 'APPLY_DISCOUNT':
return {
...state,
discount: action.payload
};
case 'CLEAR_CART':
return {
...initialState
};
case 'SET_LOADING':
return {
...state,
isLoading: action.payload
};
case 'SET_ERROR':
return {
...state,
error: action.payload,
isLoading: false
};
default:
return state;
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (id) => {
dispatch({ type: 'REMOVE_ITEM', payload: id });
};
const updateQuantity = (id, quantity) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
};
const applyDiscount = (percentage) => {
dispatch({ type: 'APPLY_DISCOUNT', payload: percentage });
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
// 计算最终价格
const finalPrice = state.totalPrice * (1 - state.discount / 100);
return (
<div>
<h2>购物车</h2>
{state.isLoading && <p>加载中...</p>}
{state.error && <p style={{ color: 'red' }}>错误: {state.error}</p>}
<div>
<h3>商品列表</h3>
<button onClick={() => addItem({ id: 1, name: '商品A', price: 100 })}>
添加商品A
</button>
<button onClick={() => addItem({ id: 2, name: '商品B', price: 200 })}>
添加商品B
</button>
</div>
<div>
<h3>购物车内容</h3>
{state.cartItems.length === 0 ? (
<p>购物车为空</p>
) : (
<ul>
{state.cartItems.map(item => (
<li key={item.id}>
{item.name} - ¥{item.price} × {item.quantity}
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>
+
</button>
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>
-
</button>
<button onClick={() => removeItem(item.id)}>删除</button>
</li>
))}
</ul>
)}
</div>
<div>
<h3>总计</h3>
<p>商品数量: {state.totalQuantity}</p>
<p>原价: ¥{state.totalPrice.toFixed(2)}</p>
<p>折扣: {state.discount}%</p>
<p>最终价格: ¥{finalPrice.toFixed(2)}</p>
</div>
<div>
<button onClick={() => applyDiscount(10)}>应用10%折扣</button>
<button onClick={() => applyDiscount(20)}>应用20%折扣</button>
<button onClick={clearCart}>清空购物车</button>
</div>
</div>
);
}
三、useState vs useReducer 选择指南
3.1 何时使用 useState
- 简单状态:独立的状态值
- 布尔值、字符串、数字:简单数据类型
- 组件内部状态:不涉及复杂逻辑
- 状态更新简单:直接设置新值
jsx
// 适合 useState 的场景
const [isOpen, setIsOpen] = useState(false);
const [name, setName] = useState('');
const [count, setCount] = useState(0);
const [selectedItem, setSelectedItem] = useState(null);
3.2 何时使用 useReducer
- 复杂状态逻辑:多个相互关联的状态值
- 状态依赖前一个状态:需要基于前状态计算
- 多个操作类型:不同的 action 类型
- 状态更新逻辑复杂:需要集中管理更新逻辑
- 需要可预测的状态更新:便于调试和测试
jsx
// 适合 useReducer 的场景
const [formState, dispatch] = useReducer(formReducer, initialFormState);
const [gameState, gameDispatch] = useReducer(gameReducer, initialGameState);
const [cartState, cartDispatch] = useReducer(cartReducer, initialCartState);
3.3 对比示例
jsx
// 使用 useState 实现表单
function FormWithUseState() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
return (
<form>
<input
name="username"
value={formData.username}
onChange={handleChange}
/>
{/* 其他字段 */}
</form>
);
}
// 使用 useReducer 实现相同的表单
const formReducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return {
...state,
[action.field]: action.value
};
case 'RESET_FORM':
return initialState;
default:
return state;
}
};
function FormWithUseReducer() {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleChange = (e) => {
const { name, value } = e.target;
dispatch({
type: 'UPDATE_FIELD',
field: name,
value: value
});
};
return (
<form>
<input
name="username"
value={state.username}
onChange={handleChange}
/>
{/* 其他字段 */}
</form>
);
}
四、高级技巧和最佳实践
4.1 自定义 Hook 封装
jsx
// 自定义 useLocalStorage Hook
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// 使用自定义 Hook
function TodoApp() {
const [todos, setTodos] = useLocalStorage('todos', []);
// ... 其他逻辑
}
// 自定义 useReducer with Logger
function useReducerWithLogger(reducer, initialState) {
const [state, dispatch] = useReducer(reducer, initialState);
const dispatchWithLogger = (action) => {
console.log('Previous State:', state);
console.log('Action:', action);
const nextState = reducer(state, action);
console.log('Next State:', nextState);
dispatch(action);
};
return [state, dispatchWithLogger];
}
4.2 性能优化
jsx
function OptimizedComponent() {
// 使用 useReducer 避免不必要的重新渲染
const [state, dispatch] = useReducer(complexReducer, initialState);
// 使用 useMemo 记忆化计算值
const computedValue = useMemo(() => {
return expensiveCalculation(state);
}, [state]);
// 使用 useCallback 记忆化函数
const handleAction = useCallback((payload) => {
dispatch({ type: 'ACTION_TYPE', payload });
}, [dispatch]);
return (
<div>
<ChildComponent onAction={handleAction} />
</div>
);
}
4.3 测试策略
jsx
// reducer 的单元测试
describe('todoReducer', () => {
const initialState = { todos: [] };
test('should add a todo', () => {
const action = {
type: 'ADD_TODO',
payload: { id: 1, text: 'Test todo', completed: false }
};
const newState = todoReducer(initialState, action);
expect(newState.todos).toHaveLength(1);
expect(newState.todos[0].text).toBe('Test todo');
});
test('should toggle a todo', () => {
const state = {
todos: [{ id: 1, text: 'Test todo', completed: false }]
};
const action = { type: 'TOGGLE_TODO', payload: 1 };
const newState = todoReducer(state, action);
expect(newState.todos[0].completed).toBe(true);
});
});
// 组件测试
test('should update count when increment button is clicked', () => {
render(<Counter />);
const incrementButton = screen.getByText('增加');
fireEvent.click(incrementButton);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
五、实战项目:待办事项应用
jsx
import React, { useReducer, useState } from 'react';
const initialState = {
todos: [],
filter: 'all', // 'all', 'active', 'completed'
nextId: 1
};
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
const newTodo = {
id: state.nextId,
text: action.payload,
completed: false,
createdAt: new Date().toISOString()
};
return {
...state,
todos: [...state.todos, newTodo],
nextId: state.nextId + 1
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
case 'EDIT_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, text: action.payload.text }
: todo
)
};
case 'SET_FILTER':
return {
...state,
filter: action.payload
};
case 'CLEAR_COMPLETED':
return {
...state,
todos: state.todos.filter(todo => !todo.completed)
};
case 'TOGGLE_ALL':
const allCompleted = state.todos.every(todo => todo.completed);
return {
...state,
todos: state.todos.map(todo => ({
...todo,
completed: !allCompleted
}))
};
default:
return state;
}
}
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, initialState);
const [inputValue, setInputValue] = useState('');
const [editingId, setEditingId] = useState(null);
const [editText, setEditText] = useState('');
// 计算属性
const filteredTodos = state.todos.filter(todo => {
if (state.filter === 'active') return !todo.completed;
if (state.filter === 'completed') return todo.completed;
return true;
});
const activeCount = state.todos.filter(todo => !todo.completed).length;
const completedCount = state.todos.length - activeCount;
// 事件处理函数
const handleAddTodo = (e) => {
e.preventDefault();
if (!inputValue.trim()) return;
dispatch({ type: 'ADD_TODO', payload: inputValue });
setInputValue('');
};
const handleEditStart = (id, text) => {
setEditingId(id);
setEditText(text);
};
const handleEditSave = (id) => {
if (!editText.trim()) {
dispatch({ type: 'DELETE_TODO', payload: id });
} else {
dispatch({ type: 'EDIT_TODO', payload: { id, text: editText } });
}
setEditingId(null);
setEditText('');
};
const handleEditCancel = () => {
setEditingId(null);
setEditText('');
};
return (
<div className="todo-app">
<header>
<h1>待办事项</h1>
<form onSubmit={handleAddTodo}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="输入新的待办事项..."
autoFocus
/>
<button type="submit">添加</button>
</form>
</header>
<main>
{state.todos.length > 0 && (
<div className="todo-controls">
<button
onClick={() => dispatch({ type: 'TOGGLE_ALL' })}
className="toggle-all"
>
{activeCount === 0 ? '取消全选' : '全选'}
</button>
<div className="filters">
<button
onClick={() => dispatch({ type: 'SET_FILTER', payload: 'all' })}
className={state.filter === 'all' ? 'active' : ''}
>
全部 ({state.todos.length})
</button>
<button
onClick={() => dispatch({ type: 'SET_FILTER', payload: 'active' })}
className={state.filter === 'active' ? 'active' : ''}
>
待办 ({activeCount})
</button>
<button
onClick={() => dispatch({ type: 'SET_FILTER', payload: 'completed' })}
className={state.filter === 'completed' ? 'active' : ''}
>
已完成 ({completedCount})
</button>
</div>
{completedCount > 0 && (
<button
onClick={() => dispatch({ type: 'CLEAR_COMPLETED' })}
className="clear-completed"
>
清除已完成
</button>
)}
</div>
)}
<ul className="todo-list">
{filteredTodos.map(todo => (
<li
key={todo.id}
className={`todo-item ${todo.completed ? 'completed' : ''} ${editingId === todo.id ? 'editing' : ''}`}
>
{editingId === todo.id ? (
<div className="edit-form">
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onBlur={() => handleEditSave(todo.id)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleEditSave(todo.id);
if (e.key === 'Escape') handleEditCancel();
}}
autoFocus
/>
<button onClick={() => handleEditSave(todo.id)}>保存</button>
<button onClick={handleEditCancel}>取消</button>
</div>
) : (
<>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
className="todo-checkbox"
/>
<span
className="todo-text"
onDoubleClick={() => handleEditStart(todo.id, todo.text)}
>
{todo.text}
</span>
<div className="todo-actions">
<button
onClick={() => handleEditStart(todo.id, todo.text)}
className="edit-btn"
>
编辑
</button>
<button
onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}
className="delete-btn"
>
删除
</button>
</div>
</>
)}
</li>
))}
</ul>
{state.todos.length === 0 && (
<div className="empty-state">
<p>暂无待办事项</p>
<p>开始添加你的第一个待办事项吧!</p>
</div>
)}
</main>
<footer>
<p>待办事项: {activeCount} 个未完成</p>
<p>当前过滤: {state.filter}</p>
</footer>
</div>
);
}
// 添加样式
const styles = `
.todo-app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
header {
text-align: center;
margin-bottom: 30px;
}
header form {
display: flex;
gap: 10px;
margin-top: 20px;
}
header input {
flex: 1;
padding: 10px;
font-size: 16px;
}
header button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
cursor: pointer;
}
.todo-controls {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #eee;
margin-bottom: 20px;
}
.filters {
display: flex;
gap: 10px;
}
.filters button.active {
background: #007bff;
color: white;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-item {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #999;
}
.todo-checkbox {
margin-right: 10px;
}
.todo-text {
flex: 1;
margin-right: 10px;
}
.todo-actions {
display: flex;
gap: 5px;
opacity: 0;
transition: opacity 0.2s;
}
.todo-item:hover .todo-actions {
opacity: 1;
}
.edit-form {
display: flex;
width: 100%;
gap: 10px;
}
.edit-form input {
flex: 1;
padding: 5px;
}
.empty-state {
text-align: center;
color: #999;
padding: 40px;
}
footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #eee;
color: #666;
font-size: 14px;
}
`;
// 在组件中添加样式
const StyledTodoApp = () => (
<>
<style>{styles}</style>
<TodoApp />
</>
);
export default StyledTodoApp;
六、总结
6.1 核心要点
-
useState
- 适合简单、独立的状态管理
- 直接设置状态值或使用函数更新
- 性能优化:惰性初始化和函数更新
-
useReducer
- 适合复杂状态逻辑和多个关联状态
- 集中管理状态更新逻辑
- 便于测试和调试
-
选择原则
- 简单状态用
useState - 复杂状态用
useReducer - 考虑组件可维护性和可测试性
- 简单状态用
6.2 最佳实践
-
命名约定
- 状态:名词(
user,todos,isLoading) - 更新函数:
setXxx或dispatch - Action:动词过去式(
ADD_TODO,UPDATE_USER)
- 状态:名词(
-
不可变性
- 始终返回新状态对象
- 不直接修改原状态
-
代码组织
- 相关状态放在一起
- 复杂 reducer 拆分为多个小函数
- 自定义 Hook 封装重复逻辑
-
性能优化
- 避免不必要的重新渲染
- 使用
useMemo和useCallback - 状态提升和状态下沉
6.3 下一步学习
useContext:全局状态管理useEffect:副作用处理- 自定义 Hook:逻辑复用
- 状态管理库:Redux、Zustand、Recoil
通过掌握 useState 和 useReducer,你已经具备了 React 状态管理的核心能力。在实际开发中,根据具体场景选择合适的方法,会让你的代码更加清晰和可维护。