前言:useState知识要点
-
初始状态的传递方式:useState(initialState) 的参数 initialState 只在组件挂载时(即初次渲染)被使用,之后的状态更新由 setState 函数控制。
- 如果传递的是一个具体的值 (如数字、字符串、对象等),React只在初次渲染时使用这个值,后续渲染时忽略。
- 如果传递的是一个函数,React在初次渲染时调用这个函数,并用返回值作为初始状态。但是,如果这个函数是直接在调用useState时定义的,那么每次渲染时都会调用这个函数,不过React只会使用初次渲染时的返回值作为初始状态,后续渲染时虽然调用但会忽略其返回值。但是,如果这个函数是作为惰性初始状态(即函数返回初始状态)的方式传递,那么React只会在初次渲染时调用一次。
- 如果初始状态来自于 props,那么当 props 改变时,useState 的状态不会自动更新,因为它只在初始渲染时使用 props 的值。 也就是说,useState 的状态是独立的,除非你使用 useEffect 来同步 props 的变化,否则状态不会随着 props 的改变而改变。
-
状态更新:
- 调用set函数来更新状态,但要注意,set函数并不会立即改变状态,而是将更新加入队列,等待下一次渲染。
- React会批量处理状态更新,这意味着在同一个事件处理函数中多次调用set函数,React会将它们合并,然后进行一次重新渲染。
-
状态更新时注意不可变性:
- 当状态是对象或数组时,应该通过创建新的对象或数组来替换旧的状态,而不是直接修改旧的状态。
-
基于先前的状态更新:
- 如果新的状态依赖于先前的状态,那么应该给set函数传递一个函数,这个函数接收先前的状态,并返回新的状态。
一、初始状态的创建时机
| 类型 | 创建时机 | 性能影响 |
|---|---|---|
| 直接值 | 只计算一次 | 无影响 |
| 函数调用结果 | 每次渲染都调用 | 可能影响性能 |
| 函数本身 | 初次渲染调用 | 优化昂贵计算 |
1. 值类型初始状态(一次性)
javascript
const [count, setCount] = useState(0); // 只在初次渲染时使用
const [obj, setObj] = useState({ a: 1, b: 2 }); // 只创建一次
2. 函数调用初始状态(每次渲染都调用)
javascript
// ❌ 每次渲染都会调用 heavyComputation()
const [value, setValue] = useState(heavyComputation());
// ✅ 使用函数式初始状态,只在初次渲染时调用
const [value, setValue] = useState(() => heavyComputation());
关键区别:当传递函数本身时,React 只在初次渲染时调用它;当传递函数调用结果时,每次渲染都会执行函数(即使返回值被忽略)。
3. props作为初始状态
javascript
function UserProfile({ initialName, initialAge }) {
// ❌ 问题:props 变化时,状态不会自动更新
const [name, setName] = useState(initialName);
const [age, setAge] = useState(initialAge);
// 如果 initialName 从 "John" 变为 "Jane",
// name 状态仍会保持 "John",不会自动更新为 "Jane"
}
核心原则 :useState 的初始值只在组件首次挂载时使用一次,后续 props 变化不会自动更新状态。
二、状态更新的异步性与批处理
1. 更新不会立即生效
javascript
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // 仍然是旧值,不是立即更新的
};
}
2. React 18+ 的自动批处理
javascript
// 所有更新都会被批处理,只触发一次重渲染
const handleClick = () => {
setCount(c => c + 1);
setName('new name');
setFlag(true);
// 这三行代码只会导致一次重渲染
};
3. 同步上下文中的批处理
javascript
// 这些场景下更新是批处理的:
// - React 事件处理器(onClick、onChange 等)
// - useEffect 回调
// - useLayoutEffect 回调
// - Promise 回调(React 18+)
// 这些场景下可能不是批处理的(React 17 及以前):
// - setTimeout、setInterval
// - 原生事件监听器
// - Promise 回调
三、不可变更新的模式
1. 对象更新:替换而非修改
javascript
const [user, setUser] = useState({ name: 'John', age: 30 });
// ❌ 错误:直接修改
user.age = 31; // 不会触发重新渲染
// ✅ 正确:创建新对象
setUser({ ...user, age: 31 });
// ✅ 深层更新
setUser(prev => ({
...prev,
profile: {
...prev.profile,
address: 'New York'
}
}));
2. 数组更新
javascript
const [items, setItems] = useState(['a', 'b', 'c']);
// 添加
setItems([...items, 'd']);
setItems(prev => [...prev, 'd']);
// 删除
setItems(items.filter((item, index) => index !== 0));
// 更新
setItems(items.map((item, index) =>
index === 1 ? 'updated' : item
));
四、函数式更新(基于先前状态)
1. 为什么需要函数式更新?
javascript
// ❌ 问题:批量更新时会有问题
const incrementTwice = () => {
setCount(count + 1); // 使用当前的 count(比如 0)
setCount(count + 1); // 仍然使用当前的 count(0)
// 结果:count 变为 1 而不是 2
};
// ✅ 解决方案:函数式更新
const incrementTwice = () => {
setCount(prev => prev + 1); // prev = 0 → 1
setCount(prev => prev + 1); // prev = 1 → 2
// 结果:count 变为 2
};
2. 复杂状态更新的模式
javascript
// 计数器队列更新
const handleBatchUpdate = () => {
setCount(prev => prev + 1);
setCount(prev => prev * 2);
setCount(prev => prev - 1);
// React 会依次执行这些更新函数
};
// 基于 props 的更新
const Counter = ({ initialValue }) => {
const [count, setCount] = useState(initialValue);
// 重置计数器
const reset = () => {
setCount(initialValue); // ❌ 如果 initialValue 变化,这里不会更新
// ✅ 使用函数式更新确保使用最新的 props
setCount(() => initialValue);
};
};
五、高级模式和最佳实践
1. 状态分割:提高性能
javascript
// ❌ 将所有状态放在一起
const [state, setState] = useState({
name: '',
email: '',
age: 0,
address: ''
});
// ✅ 分割状态,避免不必要的重渲染
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
2. 自定义 Hook 封装复杂状态逻辑
javascript
function useUndoState(initialValue) {
const [state, setState] = useState(initialValue);
const [history, setHistory] = useState([initialValue]);
const undo = () => {
if (history.length > 1) {
setHistory(prev => prev.slice(0, -1));
setState(history[history.length - 2]);
}
};
const updateState = (newValue) => {
setState(newValue);
setHistory(prev => [...prev, newValue]);
};
return [state, updateState, undo];
}
3. 状态依赖更新的优化
javascript
// 使用 useCallback 避免函数重新创建
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []); // 空依赖数组,函数只创建一次
// 使用 useMemo 避免昂贵计算
const expensiveValue = useMemo(() => {
return heavyComputation(count);
}, [count]); // 仅当 count 变化时重新计算
六、常见陷阱与解决方案
1. 闭包陷阱
javascript
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// ❌ 总是使用初始的 count(0)
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []); // 空依赖数组
// ✅ 解决方案1:使用函数式更新
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
// ✅ 解决方案2:将 count 加入依赖数组
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, [count]); // count 变化时重新创建定时器
}
2. 状态依赖其他状态的更新
javascript
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
// ❌ 错误:在事件处理器中直接设置
const handleFirstNameChange = (name) => {
setFirstName(name);
setFullName(name + ' ' + lastName); // 可能使用旧的 lastName
};
// ✅ 正确:使用 useEffect 响应状态变化
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ✅ 或者:使用 useMemo 计算派生状态
const fullName = useMemo(() => {
return firstName + ' ' + lastName;
}, [firstName, lastName]);
七、性能优化技巧
- 惰性初始状态:对于昂贵的初始计算
- 状态提升:将状态移动到最近的公共祖先
- 状态降级:使用 useMemo/useCallback 避免向下传递新的引用
- 状态合并:将频繁同时更新的状态放在一个对象中
- 状态分割:将不常变化的状态拆分出来
总结
useState 的核心原则:
- 初始状态创建:函数式初始状态只运行一次
- 不可变性:总是创建新的值,而不是修改旧值
- 函数式更新:当新状态依赖于旧状态时使用
- 批处理:React 自动优化多个状态更新
- 异步性:状态更新不会立即反映
理解这些概念不仅能帮助你写出正确的 React 代码,还能优化应用性能,避免常见的陷阱。