每个 React 开发者最终都会面临同一个问题:我应该使用 Context,还是这应该交给 Redux 来处理?如果你才开始开发几个月,网上的各种杂音会让你觉得这是一个非此即彼的选择。一些教程将 Redux 视为过时的包袱,而另一些则警告说 Context 的扩展性无法超越一个待办事项列表。这两种极端观点都无济于事。事实是,这些工具解决的是不同类型的难题,而明智的选择取决于你的应用程序实际的功能。

Prop Drilling 问题

在选择状态管理策略之前,了解这两种工具试图治愈的“病症”会有所帮助。想象一下你正在构建一个电子商务网站。你在顶层的 App 组件中获取用户资料。在底部的 footer 中,一个微小的 AccountLink 组件需要那张头像。如果没有全局 store,user 对象必须经过 Home、然后是 Header、然后是 NavContainer、然后是 UserDropdown,最后才进入 AccountLink。中间的每一层都触碰了它们并不使用的数据。这就是 Prop Drilling。

Prop Drilling 会使组件变得脆弱。重构变得具有风险,因为拔掉中间的一个环节就会破坏整个链条。复用性也会受损,因为组件被迫要求它们仅仅是为了向下传递而接收的 props。Context 和 Redux 都通过允许远程组件直接订阅共享数据来消除这个问题。但它们传递数据的方式以及这样做带来的成本,很快就会产生分歧。

什么时候 React Context API 是合适的选择

React Context 是内置在库本身的。无需额外的 npm 安装,无需构建配置,也没有样板代码文件。你创建一个 context 对象,用 Provider 包裹树的一部分,然后在任何嵌套组件中使用 useContext 来消费该值。由于这种简单性,Context 在状态变化不频繁且状态结构相对扁平的中小型项目中表现出色。

想想 UI 主题。用户可能在每次会话中只在浅色和深色模式之间切换一次。该值会传播到每个 styled component,但由于变化非常罕见,性能问题几乎可以忽略不计。身份验证状态是另一个经典的适用场景。一旦用户登录,isAuthenticated 标志和 user 对象在数十次页面导航中都保持稳定。语言或本地化设置也是如此。这些是许多组件需要、但很少组件会修改的广泛且变化缓慢的信号。

问题在于 Context 如何处理更新。当 Context Provider 的值发生变化时,React 会重新渲染每一个消费该 context 的组件。在小型应用中,你不会感觉到。但在大型应用中,如果你将快速变化的数据放在一个被广泛使用的 Context 中,你将会触发一系列浪费的重新渲染。你可以通过拆分 context 来隔离波动性,但到那时,你实际上是在手动进行优化工作,而另一种工具已经解决了这个问题。

什么时候 Redux Toolkit 值得使用

Redux Toolkit 专为状态复杂、更新频繁且多个远程功能需要读取和写入相同数据而不发生冲突的应用而设计。考虑一下购物车。用户从产品卡片中添加一个项目。页眉中的购物车图标必须更新其角标计数。一个侧边栏滑出以显示项目列表。折扣码输入框进行验证。稍后,结账页面会读取购物车内容。该状态被整个树中不相关的组件触碰,并且经常发生变化。

Redux Toolkit 通过中心化的 store 和明确的 state slices 来解决这个问题。组件使用 useSelector 仅订阅它们需要的数据片段。如果实时仪表盘中的股票价格更新,显示用户个人资料设置的组件不会被唤醒。Redux 在底层使用引用相等性检查,因此订阅是细粒度的。当你的组件数量增加到数百个时,这一点变得至关重要。

Redux 还为你提供了可预测的数据流。状态变化通过分发的 action 并由 reducer 处理。这听起来像是术语,但在实践中,这意味着你可以通过 grep 你的代码库来查找 addToCart,从而找到每一个修改购物车的代码路径。在大型团队中,这种契约可以防止 bug。相比之下,Context 只是一个值和一个 setter。任何消费者都可以调用 setState,而追踪一个错误值的来源意味着要在多个组件中设置断点。

它们真正的分歧点在哪里

性能特性是区分这些工具最显著的特征。Context 会无条件地向所有消费者广播新值。而 Redux 则仅通知其选中的 slice 发生变化的订阅者。如果你正在构建一个报价每秒刷新的实时股票仪表盘,Context 会引发全局重新渲染风暴。Redux 则能让只有行情单元格和折线图进行重新计算。

调试是 Redux 在复杂应用中更具优势的另一个领域。Redux DevTools 为你提供时间旅行调试(time-travel debugging)。你可以回溯每一个分发的 action,并观察状态的回退过程。在包含运费计算、支付验证和错误恢复的多步结账流程中,能够重现导致 bug 的确切序列是极其宝贵的。Context 依赖于标准的 React DevTools。你可以检查当前的 context 值,但它没有内置的 action 日志或状态差异(state diff)查看器。你只能重新回到到处写 console.log 的老路。

中间件与副作用是 Redux 的基因。Redux Toolkit 包含了 createAsyncThunk,并能与数据获取库完美集成。你可以在 Redux 的数据流中编排 API 调用、显示加载动画、处理网络故障并缓存结果。Context 没有为异步逻辑提供内置模式。你要么在组件内部进行获取,然后将结果推送到 Context 中,要么将 provider 封装在自制的异步工具中。这虽然可行,但属于临时凑合(ad hoc)。

设置成本是 Context 完胜的地方。构建一个主题 provider 大约只需要五分钟。Redux Toolkit 则需要创建 store 文件、定义 slices,并将你的应用包裹在 Provider 中。虽然不像以前使用旧版 Redux 及其堆积如山的样板代码(boilerplate)那样需要耗费一周时间进行繁琐的配置,但其设置工作量仍然高于 Context。对于一个周末的小型侧边项目或只有三个路由的仪表盘来说,这种开销可能并不划算。

在同一个应用中同时使用两者

你不必非要效忠于某一个阵营。许多生产级应用使用 Context 处理全局 UI 外壳(UI shell)相关的事务,而使用 Redux 处理领域密集型(domain-heavy)的业务数据。一种常见的模式是将主题、语言区域(locale)以及可能的一个轻量级身份验证标志保留在 Context 中,因为每个路由都需要它们,且它们很少发生变化。与此同时,订单管理系统、通知中心和数据表格则存在于 Redux 中,因为频繁的更新和跨组件逻辑需要精确的控制。

这种混合方法让简单的事情保持简单,而无需强行围绕一个静态的主题对象构建完整的 Redux store。它还能防止你的 Redux slices 被那些原本就不需要工业级状态管理的 UI 装饰性组件(UI chrome)所填满。

核心结论

选择更重的工具并不会让你赢得荣誉。首先观察你的状态变化频率、有多少组件会触及它,以及你是否需要跨团队边界追踪状态变更(mutations)。如果你在一个中等规模的应用中管理变化缓慢且被广泛共享的值,Context 可能就足够了。如果你的状态频繁变更、跨越不相关的特性,并且需要清晰的审计追踪(audit trail),那么 Redux Toolkit 会让你省去很多麻烦。

根据项目的实际形态来选择,而不是根据技术会议的演讲或 GitHub 的星数。一个包含 50 件商品的购物车并不意味着自动需要 Redux,而一个主题切换开关也不需要全局 store。将工具与问题匹配,这样即使在热度周期过去后,你的代码库依然能保持可维护性。