🎭 序章:一场早朝的三种剧本
想象你在 Next 的城门(pages/index.js)里开了一家茶馆:
- useState:掌柜的小黑板,今天卖多少碗面写一行。
- useReducer:账房先生的流水簿,进销存一条龙。
- useContext:茶馆广播大喇叭,后厨、跑堂、顾客都能听见。
三种工具同台唱戏,谁才是本地状态的真命天子?
咱们把舞台灯光打到底层,看 CPU 的缓存行、V8 的 Hidden Class、React Fiber 的调度队列如何为它们鼓掌。
🧭 目录(地图 & 逃生通道)
| 章节 | 关键词 | 你将学会 |
|---|---|---|
| 1️⃣ useState:一行小黑板 | 闭包、Hook 链表、调度 | 最小可观测状态 |
| 2️⃣ useReducer:账房先生 | dispatch、action、reducer | 可预测的状态机 |
| 3️⃣ useContext:茶馆广播 | Provider、Consumer、订阅 | 跨组件零 props |
| 4️⃣ 组合技:三连击 | 三件套协同 | 小而美的本地 store |
| 5️⃣ 彩蛋:性能暗器 | memo、selector、lazy | 避免重渲染地狱 |
| 6️⃣ 一键启动 | 代码模板 | 复制即可跑 |
1️⃣ useState:掌柜的小黑板
🎨 最小示例
const [count, setCount] = useState(0);
🧬 底层速写
-
Hook 链表
React 在 Fiber 节点里维护一条单向链表:hook1 → hook2 → …。
useState只是链表里的一个节点,结构形如:{ memoizedState: 0, // 当前值 queue: [], // 待执行的更新 next: hook2 // 下一个 hook } -
闭包陷阱
事件回调里如果直接写setCount(count + 1),拿到的count是旧闭包值。
用函数式更新setCount(c => c + 1)才能读到最新快照。 -
调度队列
setCount不会立即改值,而是把更新推进 Lane 队列,等浏览器下一帧再批量合并。
这就是为什么连续三次setCount(count + 1)只会加 1(除非你传函数)。
2️⃣ useReducer:账房先生的流水簿
🎨 最小示例
function counter(state, action) {
switch (action.type) {
case 'inc': return state + 1;
case 'dec': return state - 1;
default: throw new Error('未知动作');
}
}
const [state, dispatch] = useReducer(counter, 0);
🧬 底层速写
- 状态机范式
reducer = (state, action) => newState把“可变”转成“不可变快照”,天然与 Redux Devtools 打通。 - dispatch 引用稳定
与setState不同,dispatch在组件整个生命周期内 引用不变,可直接丢给子组件而不用useCallback包。 - 调试友好
每个 action 都是 可序列化 的日志,方便时间旅行调试。
3️⃣ useContext:茶馆广播
🎨 最小示例
// 创建 Context
const TeaCtx = createContext();
// Provider 在 _app.js 或组件树顶层
<TeaCtx.Provider value={{ count, setCount }}>
<Child />
</TeaCtx.Provider>
// 任意深度子组件
const { count } = useContext(TeaCtx);
🧬 底层速写
- 订阅机制
Context 本质是 单播广播器:
Provider → 把 value 存进context._currentValue
Consumer → 每次渲染读取_currentValue,若引用改变则 强制重渲染。 - 性能陷阱
把 对象字面量 直接塞给 Provider → 每次引用都不同,导致所有子树重渲染。
解决:useMemo或useReducer把 value 稳定化。
4️⃣ 组合技:三连击
🎯 场景:计数器 + 主题色切换
把三件套拼成“本地轻量 store”:
// 1. reducer 管理复杂状态
function appReducer(state, action) {
switch (action.type) {
case 'inc': return { ...state, count: state.count + 1 };
case 'dec': return { ...state, count: state.count - 1 };
case 'toggleTheme': return { ...state, dark: !state.dark };
default: return state;
}
}
// 2. Context 提供稳定引用
const AppCtx = createContext(null);
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, { count: 0, dark: false });
// useMemo 防止对象引用抖动
const value = useMemo(() => ({ state, dispatch }), [state]);
return <AppCtx.Provider value={value}>{children}</AppCtx.Provider>;
}
// 3. 任意组件消费
export function useApp() {
const ctx = useContext(AppCtx);
if (!ctx) throw new Error('useApp must be used within AppProvider');
return ctx;
}
在 Next 页面里:
export default function Home() {
return (
<AppProvider>
<Counter />
<ThemeSwitch />
</AppProvider>
);
}
5️⃣ 性能暗器
| 暗器 | 用途 | 示例 |
|---|---|---|
React.memo | 子组件 props 浅比较 | const Child = memo(({value}) => ...) |
useMemoSelector | 只订阅部分 context | const count = useMemo(() => state.count, [state.count]) |
useContextSelector (社区) | 精准订阅 | 避免大对象更新导致全树重渲染 |
6️⃣ 一键启动模板
npx create-next-app@latest local-state-demo
cd local-state-demo
# 把上面代码复制到 context/AppProvider.js
npm run dev
🏁 尾声:选择困难症的处方
- 只记一个小数?
useState一把梭。 - 状态机复杂?
useReducer把动作写进史诗。 - 深度传参地狱?
useContext上广播。
三者互不排斥,像 盐、胡椒、迷迭香——
在 Next 的厨房里,调出属于你的本地状态美味!