用 React Context + useReducer 实现轻量全局状态:不装 Redux 也能管好状态

📅 2026/7/10 12:55:19
用 React Context + useReducer 实现轻量全局状态:不装 Redux 也能管好状态
用 React Context useReducer 实现轻量全局状态:不装 Redux 也能管好状态不是每个项目都需要 Redux。用户登录信息、主题切换、购物车、一个多步表单的数据——这些跨组件共享的状态,用 React 自带的ContextuseReducer就能管得很干净,还不用引第三方库。但很多人第一次这么写会踩两个坑:要么把逻辑写得散乱、组件里到处dispatch;要么 Context 一变全树重渲染,性能崩了。这篇从「为什么要这么组合」讲起,给出一套可直接抄进项目的写法,并解决那个最要命的重渲染问题。先说清楚:Context 和 useReducer 各管什么这俩解决的是不同的问题,组合起来才完整:useReducer管「状态怎么变」——把所有更新逻辑收拢到一个 reducer 函数里,组件只负责dispatch一个动作,不关心细节。Context管「状态给谁用」——让深层嵌套的组件不用一层层传 props(避免 prop drilling)就能拿到状态和 dispatch。单用 Context 传一个useState也行,但状态更新逻辑一复杂就乱;useReducer正好补上这块。从一个真实场景开始:购物车先看没有全局状态时的痛点。购物车状态在顶层,加购按钮在商品卡片里,数量显示在导航栏里——三个组件隔着好几层。用 props 传,得把cart和setCart从顶层一路透传下去,中间那些根本不关心购物车的组件被迫当二传手。我们用 Context useReducer 重构。第一步,定义 reducer,把所有购物车操作集中起来:// cartReducer.js export const initialState { items: [] }; export function cartReducer(state, action) { switch (action.type) { case ADD: { const exist state.items.find(i i.id action.item.id); if (exist) { // 已存在则数量 1,而不是重复添加 return { items: state.items.map(i i.id action.item.id ? { ...i, qty: i.qty 1 } : i ), }; } return { items: [...state.items, { ...action.item, qty: 1 }] }; } case REMOVE: return { items: state.items.filter(i i.id ! action.id) }; case CLEAR: return { items: [] }; default: // 兜底:遇到未知 action 直接返回原状态,别静默改错 return state; } }reducer 是纯函数,输入旧状态和 action,输出新状态。所有「加购要不要合并、删除怎么删」的逻辑都在这里,组件里看不到这些细节。第二步:用 Context 把状态和 dispatch 提供出去// CartContext.jsx import { createContext, useContext, useReducer } from react; import { cartReducer, initialState } from ./cartReducer; const CartContext createContext(null); export function CartProvider({ children }) { const [state, dispatch] useReducer(cartReducer, initialState); return ( CartContext.Provider value{{ state, dispatch }} {children} /CartContext.Provider ); } // 封装一个自定义 Hook,组件用起来更顺,还能顺手做「忘了包 Provider」的校验 export function useCart() { const ctx useContext(CartContext); if (!ctx) { throw new Error(useCart 必须在 CartProvider 内部使用); } return ctx; }把useContext包成useCart自定义 Hook 是个好习惯:组件里const { state, dispatch } useCart()一行搞定,而且忘了套 Provider 时会立刻抛出清晰的错误,而不是拿到null后在别处报一个莫名其妙的错。第三步:在组件里消费顶层套上 Provider:// App.jsx import { CartProvider } from ./CartContext; export default function App() { return ( CartProvider Navbar / ProductList / /CartProvider ); }商品卡片里加购,导航栏里显示数量——都不需要透传 props,各自useCart即可:// ProductCard.jsx import { useCart } from ./CartContext; function ProductCard({ product }) { const { dispatch } useCart(); return ( button onClick{() dispatch({ type: ADD, item: product })} 加入购物车 /button ); }// Navbar.jsx import { useCart } from ./CartContext; function Navbar() { const { state } useCart(); const count state.items.reduce((sum, i) sum i.qty, 0); return div购物车({count})/div; }到这里功能就通了。但有个性能问题埋在下面。关键坑:Context 一变,全树重渲染上面的写法有个隐患:value{{ state, dispatch }}每次CartProvider渲染都会创建一个新对象,而且 state 一变,所有调用useCart的组件都会重渲染——哪怕某个组件只用了dispatch、根本不关心 state 变化。购物车小还好,状态一大、消费组件一多,就会出现「改一个字段全页面重渲」的卡顿。解法:把 state 和 dispatch 拆成两个 Context。dispatch 是稳定不变的,只用 dispatch 的组件就不会因 state 变化而重渲:const CartStateContext createContext(null); const CartDispatchContext createContext(null); export function CartProvider({ children }) { const [state, dispatch] useReducer(cartReducer, initialState); return ( CartStateContext.Provider value{state} CartDispatchContext.Provider value{dispatch} {children} /CartDispatchContext.Provider /CartStateContext.Provider ); } export function useCartState() { const ctx useContext(CartStateContext); if (ctx null) throw new Error(useCartState 必须在 CartProvider 内); return ctx; } export function useCartDispatch() { const ctx useContext(CartDispatchContext); if (!ctx) throw new Error(useCartDispatch 必须在 CartProvider 内); return ctx; }现在ProductCard只useCartDispatch(),state 怎么变它都不重渲;Navbar用useCartState(),只在 state 变时更新。dispatch的引用被 React 保证在整个生命周期里稳定,所以拆开后天然优化。React 官方文档也推荐这种「State Dispatch 双 Context」的写法,是社区里 ContextuseReducer 方案的标配。什么时候该上 Redux / ZustandContext useReducer 不是万能的,以下情况考虑更专业的库:状态更新非常频繁(如实时协作、动画),即使拆了 Context 也扛不住,需要更细粒度的订阅(Zustand、Jotai 的 selector 机制)。需要中间件生态:redux-persist 持久化、devtools 时间旅行调试、复杂异步流(redux-saga)。状态逻辑跨很多模块,需要更强的组织约束。反过来,中小型应用、状态更新不密集,Context useReducer 足够了,还省一个依赖。别一上来就 Redux over-engineering。小结useReducer管「怎么变」(逻辑收拢进 reducer 纯函数),Context管「给谁用」(消除 prop drilling),两者组合是 React 内置的轻量全局状态方案。把useContext封装成自定义 Hook(useCart),用起来更顺,还能校验 Provider 是否遗漏。最大的坑是重渲染:value每次新建对象 state 变化触发全体消费者重渲。解法是拆成 State 和 Dispatch 两个 Context,只用 dispatch 的组件就不受 state 变化影响。按规模选型:更新不密集的中小应用用 ContextuseReducer;高频更新或要中间件生态再上 Zustand/Redux。一句话记忆:Context 负责「送达」,useReducer 负责「变更」;性能卡了就把 state 和 dispatch 拆两个 Context——这是内置方案的天花板,够用就别上库。