Skip to content

第四章:React 只管 View,剩下的世界怎么办 ​

React 解决了一个局部而关键的问题:给定 props 与 state,如何声明 UI,并把变化落实到宿主环境。它没有回答应用级状态从哪里来、多个模型如何协同、网络结果怎样缓存、URL 如何驱动页面。

这既是设计边界,也是现实策略。Facebook 不可能要求所有已有数据层先迁移,外部开发者也更容易把 React 放进 Backbone 或服务端页面的一块区域。但当组件数量增多,“只做 View”很快把旧问题以新形式暴露出来:如果多个组件都能修改共享模型,单向的组件树并不能自动带来单向的数据流。

双向绑定为何再次变得危险 ​

2014 年官方文章《Flux: An Application Architecture for React》把压力说得很直接:双向绑定会产生级联更新,一个 model 改变另一个 model,单次交互最终影响什么变得难以预测。

Flux 的回答不是给 React 增加更多 lifecycle,而是在组件树之外规定一条单向路径:

text
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 与最终宿主操作应该是两个层次。

Engineering history, reconstructed from primary sources.