当前位置: 首页 > news >正文

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

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

不是每个项目都需要 Redux。用户登录信息、主题切换、购物车、一个多步表单的数据——这些跨组件共享的状态,用 React 自带的Context+useReducer就能管得很干净,还不用引第三方库。

但很多人第一次这么写会踩两个坑:要么把逻辑写得散乱、组件里到处dispatch;要么 Context 一变全树重渲染,性能崩了。这篇从「为什么要这么组合」讲起,给出一套可直接抄进项目的写法,并解决那个最要命的重渲染问题。

先说清楚:Context 和 useReducer 各管什么

这俩解决的是不同的问题,组合起来才完整:

  • useReducer管「状态怎么变」——把所有更新逻辑收拢到一个 reducer 函数里,组件只负责dispatch一个动作,不关心细节。
  • Context管「状态给谁用」——让深层嵌套的组件不用一层层传 props(避免 prop drilling)就能拿到状态和 dispatch。

单用 Context 传一个useState也行,但状态更新逻辑一复杂就乱;useReducer正好补上这块。

从一个真实场景开始:购物车

先看没有全局状态时的痛点。购物车状态在顶层,加购按钮在商品卡片里,数量显示在导航栏里——三个组件隔着好几层。用 props 传,得把cartsetCart从顶层一路透传下去,中间那些根本不关心购物车的组件被迫当二传手。

我们用 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; }

现在ProductCarduseCartDispatch(),state 怎么变它都不重渲;NavbaruseCartState(),只在 state 变时更新。dispatch的引用被 React 保证在整个生命周期里稳定,所以拆开后天然优化。

React 官方文档也推荐这种「State + Dispatch 双 Context」的写法,是社区里 Context+useReducer 方案的标配。

什么时候该上 Redux / Zustand

Context + 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 变化影响。
  • 按规模选型:更新不密集的中小应用用 Context+useReducer;高频更新或要中间件生态再上 Zustand/Redux。

一句话记忆:Context 负责「送达」,useReducer 负责「变更」;性能卡了就把 state 和 dispatch 拆两个 Context——这是内置方案的天花板,够用就别上库。

http://www.jsqmd.com/news/1160975/

相关文章:

  • 开关电源三种控制模式:PWM/PFM/PSM
  • 提示词工程实战:七大技巧让你的AI输出质量翻倍(附完整代码)
  • PyCharm 使用笔记
  • 积家中国官方售后服务体系全攻略|最新维修地址及电话权威收录(2026年7月最新) - 积家中国服务中心
  • 机器学习中的觉的距离和相似度
  • 终极指南:5分钟解决Windows游戏和软件VC++运行库问题
  • PyMongo 4.x 时区配置实战:CodecOptions 实现 UTC 与本地时间自动转换
  • 170、标签分配改进全景总结:四大分配器源码级对比与最佳配置推荐
  • 玄学中的系统论:从周期律到模型训练的收敛震荡
  • C++ 类型转换
  • 维度建模实战:电商数仓 4 层架构设计,从 ODS 到 ADS 的完整落地流程
  • 2026年医疗器械EMC安规检测服务商盘点、实力解析与合规选型避坑指南 - 产业观察报
  • STM32F429ZI与EM3080-W条形码识别系统设计
  • 秒杀系统如何设计
  • RedisDesktopManager Windows终极指南:告别命令行,3分钟搭建可视化Redis管理平台
  • 3分钟极速体验:这款免费浏览器扩展让你的GitHub下载速度飙升10倍
  • 终极指南:如何用MCA Selector轻松管理你的Minecraft世界存档
  • 高精度ADC系统设计:ADS131M02与PIC18F86J15的工程实践
  • 程序员必读书单
  • d3d8to9:让Direct3D 8游戏在Windows 10/11上完美运行的终极解决方案
  • 数学学习指南
  • 江诗丹顿中国官方售后服务中心|最新网点地址及官网热线权威公布(2026年7月最新) - 江诗丹顿中国服务中心
  • OneMore插件技术解析:深度重构OneNote生产力的架构设计与实现路径
  • 化工涂料公司怎么做GEO:AI引用逻辑、服务商选择、具体操作、效果监控全流程分享,全程零代码SAAS
  • JavaScript 网盘文件直链获取技术方案:重构下载体验的现代实现
  • PLIP完全指南:如何通过3种方法高效分析蛋白质-配体相互作用
  • 直流有刷电机驱动器TC78H651AFNG与PIC18LF47K40应用解析
  • C++ 运行问题汇总
  • 免费论文下载网址
  • Ubuntu 终端