第二章:React原生状态——从useState到Context API的深度解析
大家好,我是你们这门课的主讲。今天咱们来聊聊React最基础、也最容易踩坑的部分——原生状态管理。
说实话,我见过太多项目,明明用useState就能搞定的事,非要上Redux。也见过相反的情况——一个简单的Context被滥用,导致整个应用卡成PPT。嗯,今天我们就来把这些坑一个个填平。
2.1 useState:比你想象的更复杂
先说说useState。很多人觉得它简单,不就是存个状态嘛。但我在项目中遇到过好几次线上bug,追根溯源都是useState用错了。
2.1.1 状态更新的异步陷阱
看这段代码,你觉得会输出什么?
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
console.log(count); // 输出多少?
}
答案是0。而且count最终只加了1,不是2。
为什么会这样?因为React的状态更新是异步的。同一个事件循环里的多次setCount会被合并。说白了,两次setCount拿到的count都是旧值0。
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 这样就能加2了
2.1.2 对象状态的更新
我个人习惯,只要状态是对象,一定用useReducer或者immer。为什么?看这个:
const [user, setUser] = useState({ name: '张三', age: 25 });
// 错误写法
setUser({ name: '李四' }); // age丢了!
// 正确写法
setUser(prev => ({ ...prev, name: '李四' }));
你想想看,如果对象嵌套三层以上,每次都要手动展开,代码能看吗?我建议:对象超过两层,直接上useReducer。
2.2 useReducer:状态逻辑的救星
useReducer不是Redux的替代品,它是React自带的。说白了,它就是useState的升级版——当状态逻辑复杂时,用reducer把逻辑集中管理。
2.2.1 什么时候该用?
我有个简单的判断标准:
- 状态有多个子值,且更新逻辑相互依赖 → 用useReducer
- 状态更新需要依赖前一个状态 → 用useReducer
- 状态逻辑可以复用 → 用useReducer
举个例子,一个表单状态:
const initialState = {
name: '',
email: '',
age: 0,
errors: {}
};
function formReducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
[action.field]: action.value,
errors: { ...state.errors, [action.field]: null }
};
case 'SET_ERRORS':
return { ...state, errors: action.errors };
case 'RESET':
return initialState;
default:
return state;
}
}
// 使用
const [state, dispatch] = useReducer(formReducer, initialState);
dispatch({ type: 'SET_FIELD', field: 'name', value: '李四' });
你看,所有更新逻辑都集中在reducer里,组件只负责派发动作。维护起来舒服多了。
2.3 Context API:性能陷阱与正确姿势
Context API是个好东西,但用不好就是性能杀手。我曾经在一个项目中,把用户信息放在顶层Context里,结果每次用户信息一更新,整个应用都重新渲染——那叫一个酸爽。
2.3.1 原理:为什么Context会导致重渲染?
Context的机制是这样的:当Context的值变化时,所有消费该Context的组件都会重新渲染。注意,是所有,不管你有没有用到变化的那部分数据。
举个例子:
const AppContext = React.createContext();
function App() {
const [user, setUser] = useState({ name: '张三' });
const [theme, setTheme] = useState('light');
return (
<AppContext.Provider value={{ user, theme, setUser, setTheme }}>
<UserProfile />
<ThemeToggle />
</AppContext.Provider>
);
}
function ThemeToggle() {
const { theme, setTheme } = useContext(AppContext);
// 这里只用了theme,但user变了它也会重渲染!
return <button>{theme}</button>;
}
为什么会这样?因为每次setUser都会创建新的value对象,导致所有消费者都认为Context变了。
2.3.2 解决方案:拆分Context
我建议的做法是:把不相关的状态拆成多个Context。
const UserContext = React.createContext();
const ThemeContext = React.createContext();
function App() {
const [user, setUser] = useState({ name: '张三' });
const [theme, setTheme] = useState('light');
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<UserProfile />
<ThemeToggle />
</ThemeContext.Provider>
</UserContext.Provider>
);
}
// 现在ThemeToggle只订阅ThemeContext,user变了它不会重渲染
2.3.3 另一个技巧:用useMemo缓存value
如果实在不想拆分,可以用useMemo:
const value = useMemo(() => ({ user, setUser }), [user]);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
这样只有当user变化时,value引用才会变。但说实话,这只是治标不治本。如果状态真的很多,拆分才是正道。
2.4 实战建议:如何选择?
最后,我给大家一个决策树:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 单个简单值(数字、字符串) | useState | 够用,简单 |
| 对象状态,更新逻辑简单 | useState + 展开运算符 | 代码量少 |
| 对象状态,更新逻辑复杂 | useReducer | 逻辑集中,可测试 |
| 跨组件共享状态,层级浅 | Context + useReducer | 避免prop drilling |
| 跨组件共享状态,层级深 | 考虑zustand/jotai | Context性能问题 |
记住,没有银弹。我见过用Redux写todo list的,也见过用useState管理整个应用状态的。关键是:选择最适合当前场景的方案,而不是最流行的方案。
好了,这一章就到这里。下一章我们会聊聊第三方状态库——从zustand到jotai,看看它们是怎么解决React原生状态的那些痛点的。