第四章:React 只管 View,剩下的世界怎么办
React 解决了一个局部而关键的问题:给定 props 与 state,如何声明 UI,并把变化落实到宿主环境。它没有回答应用级状态从哪里来、多个模型如何协同、网络结果怎样缓存、URL 如何驱动页面。
这既是设计边界,也是现实策略。Facebook 不可能要求所有已有数据层先迁移,外部开发者也更容易把 React 放进 Backbone 或服务端页面的一块区域。但当组件数量增多,“只做 View”很快把旧问题以新形式暴露出来:如果多个组件都能修改共享模型,单向的组件树并不能自动带来单向的数据流。
双向绑定为何再次变得危险
2014 年官方文章《Flux: An Application Architecture for React》把压力说得很直接:双向绑定会产生级联更新,一个 model 改变另一个 model,单次交互最终影响什么变得难以预测。
Flux 的回答不是给 React 增加更多 lifecycle,而是在组件树之外规定一条单向路径:
Action → Dispatcher → Store → View
↑ │
└──────── user event ─────┘View 把交互转成 action;单例 dispatcher 按顺序通知 store;store 保存业务数据并发出变更;React view 重新读取数据并 render。重要的不是每个名词,而是更新不应在任意 model 间双向传播。所有变化先成为可观察的 action,再沿约定方向流动。
官方也明确说 Flux 更像 pattern,而不是 formal framework。这保留了实现自由,却意味着生态会产生大量不同答案:dispatcher 是否必要?store 是单例还是树状?异步逻辑放 action creator、middleware 还是数据层?派生数据该缓存在哪里?
React 的单向性只覆盖一半系统
React props 从父向子流动,子组件通过 callback 把意图交回上层。这个结构容易让人认为 React 天然解决了应用状态。但组件 state、外部 store、服务器缓存和 URL 仍可能相互影响。
Flux 把“谁可以改变数据”从组件层抽离,React 则继续只负责“数据改变后 UI 是什么”。两者组合形成当时 Facebook 的完整叙事,但 React 核心没有把 Flux 固化为唯一方案。
本文判断:这是 React 生态最重要也最矛盾的选择之一。核心保持狭窄,允许 Redux、MobX、Relay、GraphQL client 和各种 router 自由竞争;同一自由也让“创建一个生产应用”长期缺少统一答案。
Redux:社区把 Flux 再压缩一次
2015 年出现的 Redux 受 Flux、Elm 和函数式思想影响,把多个 store 与 dispatcher 收敛为单一 store、纯 reducer 和 action。它不依赖 React,却因 React 的可重复 render 与不可变更新习惯而迅速结合。Redux 的时间旅行调试之所以可能,正是因为 action 与 reducer 把状态迁移表示成显式数据和函数。
Redux 并不是 React 团队的官方替代核心。它的成功反而证明 React 提供了一条稳定接缝:只要外部状态容器能通知组件并提供快照,React 不关心它内部采用事件、observable、immutable tree 还是查询缓存。
随着使用规模扩大,Redux 也暴露模板代码、异步编排和规范过多的问题,后来官方以 Redux Toolkit 收敛最佳实践。这个循环——一个极简核心催生自由扩展,扩展变多后再由更高层工具收束——此后会在 React tooling 和 framework 上重复。
Mixin、HOC 与 Render Props:逻辑复用的漫长绕路
应用状态只是复用问题的一部分。React.createClass 时代,mixin 可以把 lifecycle 和方法合入多个组件。它便于共享订阅、store 连接或事件逻辑,却会产生隐式依赖:两个 mixin 可能定义同名方法,一个 mixin 可能要求组件预先存在某字段,数据来源在实例上难以追踪。
ES6 class 进入 React 后没有原生 mixin,生态转向 Higher-Order Component:函数接收组件,返回包装组件。HOC 显式了组合关系,但大量 connect、withRouter、withTheme 会形成 wrapper hierarchy,影响 props 命名、类型推断、refs 与调试。
Render Props 又把共享逻辑放进一个组件,通过函数 child 暴露值。它减少了命名冲突,却把控制流嵌进 JSX,层层嵌套时可读性下降。
这些方案都不是错误。它们说明 class component 把 state 与 lifecycle 锁在实例上,而 JavaScript 缺少一种直接复用“带状态逻辑、但不增加组件层级”的原语。Hooks 的设计压力正来自多年生态实验,而不是突然偏好函数写法。
Relay:当数据依赖靠近组件
Flux 解决客户端数据流,却没有解决服务器数据需求与组件树之间的距离。Facebook 后来的 Relay 让组件声明数据 fragment,并由框架聚合、请求和缓存。GraphQL 与 React 的结合再次体现同一原则:组件描述自己需要什么,高层运行时负责把分散声明组织成有效执行计划。
Relay 没有进入 React core,但它预演了 Server Components 的若干问题:数据依赖能否与组件共置?怎样避免父子组件串行请求?模块与数据怎样一起分割?服务器和客户端各自执行哪部分?
工具链碎片化与 Create React App
JSX、ES modules、开发服务器、lint、测试和生产优化需要一套构建配置。早期每个项目都要自己组合 Babel、Webpack、ESLint 等工具;boilerplate 很快分叉,升级困难。2016 年 Create React App 用一个受维护的默认配置隐藏细节,让新应用可以直接开始写组件。
它解决的是生态自由带来的入门成本,也给 React 团队提供统一分发新 JSX transform、Fast Refresh 和 Hooks lint rule 的渠道。但 CRA 只提供 client-side build,不负责 routing、data fetching、SSR、streaming 和 deployment。随着 React 的能力越来越依赖这些环节,单纯 build tool 最终不足以承载推荐架构。2025 年 React 团队宣布停止把 CRA 用于新应用,转而推荐 framework 或 Vite、Parcel、Rsbuild 等构建工具。
这不是简单的工具过时,而是 React 边界变化的证据:2013 年“只做 View”足以定义核心;到 Server Components 与 Actions 时代,路由、数据、代码分割、服务器执行和缓存彼此耦合,框架成为功能落地的一部分。
核心克制的收益
React 没有把 Flux 或 Redux 内置,带来几项长期收益:
- 旧应用可以逐步采用,不必先替换全部架构。
- 状态管理创新不受 React 发布节奏限制。
- renderer 可以在 Web、Native、测试和其他宿主上共享组件模型。
- React 团队能集中维护 element、reconciler、scheduler 与宿主契约。
同时,应用开发者承担了集成成本:库之间的 tearing、重复缓存、生命周期不一致和升级组合都需要处理。React 18 为外部 store 增加 useSyncExternalStore,就是 core 最终不得不为并发语义定义一条正式接缝的例子。
本文判断:React 所谓“只是一个库”从来不等于影响范围小。它通过规定 render 和 component identity,塑造了周边所有库的接口;只是把业务架构的具体选择留在 core 之外。
下一章会看到,这种狭窄核心为何能跨出浏览器。React Native 并非给 ReactDOM 换一组标签,而是迫使项目公开承认:React 的本质不在 DOM,reconciliation 与最终宿主操作应该是两个层次。