🧠《Next 本地状态三重奏:useState、useReducer、useContext 的宫廷大戏》

99 阅读3分钟

🎭 序章:一场早朝的三种剧本

想象你在 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);

🧬 底层速写

  1. Hook 链表
    React 在 Fiber 节点里维护一条单向链表:hook1 → hook2 → …
    useState 只是链表里的一个节点,结构形如:

    {
      memoizedState: 0,     // 当前值
      queue: [],            // 待执行的更新
      next: hook2           // 下一个 hook
    }
    
  2. 闭包陷阱
    事件回调里如果直接写 setCount(count + 1),拿到的 count 是旧闭包值。
    用函数式更新 setCount(c => c + 1) 才能读到最新快照。

  3. 调度队列
    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);

🧬 底层速写

  1. 状态机范式
    reducer = (state, action) => newState 把“可变”转成“不可变快照”,天然与 Redux Devtools 打通。
  2. dispatch 引用稳定
    setState 不同,dispatch 在组件整个生命周期内 引用不变,可直接丢给子组件而不用 useCallback 包。
  3. 调试友好
    每个 action 都是 可序列化 的日志,方便时间旅行调试。

3️⃣ useContext:茶馆广播

🎨 最小示例

// 创建 Context
const TeaCtx = createContext();

// Provider 在 _app.js 或组件树顶层
<TeaCtx.Provider value={{ count, setCount }}>
  <Child />
</TeaCtx.Provider>

// 任意深度子组件
const { count } = useContext(TeaCtx);

🧬 底层速写

  1. 订阅机制
    Context 本质是 单播广播器
    Provider → 把 value 存进 context._currentValue
    Consumer → 每次渲染读取 _currentValue,若引用改变则 强制重渲染
  2. 性能陷阱
    对象字面量 直接塞给 Provider → 每次引用都不同,导致所有子树重渲染。
    解决:useMemouseReducer 把 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只订阅部分 contextconst 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 的厨房里,调出属于你的本地状态美味!