useState 深入与最佳实践
一、useState 基础
1.1 基本语法
const [state, setState] = useState(initialState);state:当前的状态值setState:更新状态的函数initialState:初始值(可以是任意类型)
1.2 基础示例
function Counter() { const [count, setCount] = useState(0); return ( <div> <p>点击了 {count} 次</p> <button onClick={() => setCount(count + 1)}>增加</button> </div> ); }二、初始化状态
2.1 直接传入初始值
// 简单值 const [count, setCount] = useState(0); const [name, setName] = useState(''); const [user, setUser] = useState(null); const [items, setItems] = useState([]);2.2 惰性初始化(Lazy Initialization)
当初始值需要复杂计算时,使用函数形式可以避免每次渲染都重新计算。
// ❌ 每次渲染都会执行 expensiveComputation const [state, setState] = useState(expensiveComputation()); // ✅ 只在首次渲染时执行一次 const [state, setState] = useState(() => expensiveComputation());实际应用:
function ExpensiveComponent() { // 从 localStorage 读取,只在初始化时执行一次 const [todos, setTodos] = useState(() => { const saved = localStorage.getItem('todos'); return saved ? JSON.parse(saved) : []; }); // 复杂计算 const [stats, setStats] = useState(() => { return calculateExpensiveStats(initialData); }); }三、更新状态
3.1 直接设置新值
function SimpleUpdate() { const [count, setCount] = useState(0); const [name, setName] = useState(''); const increment = () => setCount(5); // 直接设置 const changeName = () => setName('张三'); }3.2 基于上一次状态更新(函数式更新)
当新状态依赖于旧状态时,使用函数式更新可以避免闭包陷阱。
function Counter() { const [count, setCount] = useState(0); // ❌ 错误:可能拿到过期的 count const incrementTwice = () => { setCount(count + 1); setCount(count + 1); // 只会增加 1 }; // ✅ 正确:使用函数式更新 const incrementTwiceCorrect = () => { setCount(prev => prev + 1); setCount(prev => prev + 1); // 增加 2 }; return ( <div> <p>Count: {count}</p> <button onClick={incrementTwiceCorrect}>+2</button> </div> ); }3.3 对象和数组的更新(不可变性)
function ObjectState() { const [user, setUser] = useState({ name: '张三', age: 25 }); // ✅ 更新对象:创建新对象 const updateName = () => { setUser(prev => ({ ...prev, name: '李四' })); }; const [todos, setTodos] = useState([ { id: 1, text: '学习 React', done: false } ]); // ✅ 添加项 const addTodo = (text) => { setTodos(prev => [...prev, { id: Date.now(), text, done: false }]); }; // ✅ 删除项 const deleteTodo = (id) => { setTodos(prev => prev.filter(todo => todo.id !== id)); }; // ✅ 更新项 const toggleTodo = (id) => { setTodos(prev => prev.map(todo => todo.id === id ? { ...todo, done: !todo.done } : todo )); }; }四、常见使用场景
4.1 表单状态管理
function RegistrationForm() { 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> ); }4.2 多个独立状态 vs 单个对象状态
// 方式一:多个独立状态(推荐,相关性强时) function MultiState() { const [name, setName] = useState(''); const [age, setAge] = useState(0); const [email, setEmail] = useState(''); } // 方式二:单个对象状态(推荐,相关性强时) function ObjectState() { const [user, setUser] = useState({ name: '', age: 0, email: '' }); const updateUser = (field, value) => { setUser(prev => ({ ...prev, [field]: value })); }; } // 方式三:useReducer(复杂状态逻辑) function ComplexState() { const [state, dispatch] = useReducer(reducer, initialState); }4.3 布尔状态切换
function Modal() { const [isOpen, setIsOpen] = useState(false); const open = () => setIsOpen(true); const close = () => setIsOpen(false); const toggle = () => setIsOpen(prev => !prev); return ( <> <button onClick={toggle}>切换弹窗</button> {isOpen && <div className="modal">弹窗内容</div>} </> ); }4.4 状态重置
function ResettableCounter({ initialCount = 0 }) { const [count, setCount] = useState(initialCount); // 当 initialCount 变化时重置状态 useEffect(() => { setCount(initialCount); }, [initialCount]); // 更好的方式:使用 key return <Counter key={initialCount} initialCount={initialCount} />; }五、性能优化
5.1 避免不必要的状态更新
function SearchInput() { const [value, setValue] = useState(''); // ✅ 使用防抖减少状态更新频率 const debouncedSetValue = useMemo( () => debounce(setValue, 300), [] ); const handleChange = (e) => { debouncedSetValue(e.target.value); }; return <input onChange={handleChange} />; }5.2 批量更新
function BatchUpdate() { const [count, setCount] = useState(0); const [flag, setFlag] = useState(false); // React 18+ 自动批量更新 const handleClick = () => { setCount(c => c + 1); // 不会立即触发渲染 setFlag(f => !f); // 合并为一次渲染 setCount(c => c + 1); // 仍然合并 }; // 需要立即更新时使用 flushSync const handleFlushSync = () => { flushSync(() => { setCount(c => c + 1); // 立即渲染 }); // DOM 已更新 setFlag(f => !f); // 第二次渲染 }; }5.3 状态拆分原则
// ❌ 不好的做法:不相关的状态放在一起 const [state, setState] = useState({ count: 0, name: '', isOpen: false, data: [] }); // ✅ 好的做法:按功能拆分 const [count, setCount] = useState(0); const [name, setName] = useState(''); const [isOpen, setIsOpen] = useState(false); const [data, setData] = useState([]);六、常见陷阱与解决方案
6.1 闭包陷阱
function ClosureTrap() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { // ❌ count 是闭包中的旧值,始终是 0 setCount(count + 1); }, 1000); return () => clearInterval(timer); }, []); // 依赖数组为空 // ✅ 使用函数式更新 useEffect(() => { const timer = setInterval(() => { setCount(prev => prev + 1); }, 1000); return () => clearInterval(timer); }, []); }6.2 异步状态访问
function AsyncState() { const [count, setCount] = useState(0); const handleClick = async () => { setCount(count + 1); // ❌ 这里打印的是旧值 console.log(count); // 还是原来的值 // ✅ 使用 useEffect 监听变化 useEffect(() => { console.log('count 已更新:', count); }, [count]); }; }6.3 状态更新合并
function ObjectUpdate() { const [user, setUser] = useState({ name: '张三', age: 25 }); // ❌ 这会覆盖整个对象 const wrongUpdate = () => { setUser({ age: 26 }); // name 丢失了! }; // ✅ 正确:展开运算符 const correctUpdate = () => { setUser(prev => ({ ...prev, age: 26 })); }; }七、与类组件 state 的对比
| 特性 | 类组件 state | 函数组件 useState |
|---|---|---|
| 定义 | this.state = { ... } | const [state, setState] = useState() |
| 更新 | this.setState({ ... }) | setState(newValue) |
| 合并 | 自动浅合并 | 不会自动合并 |
| 读取 | this.state.count | count |
| 多个状态 | 一个对象 | 可多个独立状态 |
八、练习题
基础题
- 实现一个计数器,支持 +1、-1、重置
- 实现一个待办事项列表,支持添加、删除、标记完成
进阶题
- 实现一个购物车,支持添加商品、修改数量、删除商品、计算总价
- 实现一个表单,包含用户名、邮箱、密码,提交时验证
参考答案
// 1. 完整计数器 function Counter() { const [count, setCount] = useState(0); return ( <div> <p>当前计数: {count}</p> <button onClick={() => setCount(prev => prev + 1)}>+1</button> <button onClick={() => setCount(prev => prev - 1)}>-1</button> <button onClick={() => setCount(0)}>重置</button> </div> ); } // 2. 购物车 function ShoppingCart() { const [cart, setCart] = useState([]); const addItem = (item) => { setCart(prev => { const existing = prev.find(i => i.id === item.id); if (existing) { return prev.map(i => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i ); } return [...prev, { ...item, quantity: 1 }]; }); }; const updateQuantity = (id, delta) => { setCart(prev => prev.map(item => item.id === id ? { ...item, quantity: Math.max(0, item.quantity + delta) } : item ).filter(item => item.quantity > 0) ); }; const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0); return ( <div> {cart.map(item => ( <div key={item.id}> {item.name} - ¥{item.price} x {item.quantity} <button onClick={() => updateQuantity(item.id, 1)}>+</button> <button onClick={() => updateQuantity(item.id, -1)}>-</button> </div> ))} <p>总计: ¥{total}</p> </div> ); }九、小结
| 要点 | 说明 |
|---|---|
| 惰性初始化 | 复杂初始值使用函数形式 |
| 函数式更新 | 新状态依赖旧状态时使用 |
| 不可变性 | 永远不要直接修改状态 |
| 状态拆分 | 不相关的状态分开管理 |
| 闭包陷阱 | 定时器/事件中使用函数式更新 |
核心要点:
- useState 是 React 最基础的 Hook
- 理解不可变更新的重要性
- 正确使用函数式更新避免闭包问题
- 根据相关性决定是否拆分状态