React 发展史:从 Facebook 内部工具到前端基础设施
目录
一段“退步”的代码,重写了前端
2013 年 5 月,React 在 JSConf US 对外亮相。台上的代码里,JavaScript 和一种酷似 HTML 的标记挤在同一个文件。今天的前端开发者看到 <Comment author={user}> 大概不会抬一下眉毛,当时却不是这样。过去十几年,Web 开发一直在教育人们把结构、样式和行为分开:HTML 管结构,CSS 管外观,JavaScript 管交互。React 端上来的 JSX 像是把刚刚整理好的抽屉又一把推乱。
更可疑的是它的更新办法:数据一变,就再执行一次 render,把界面重新描述一遍;React 自己比较前后差异,再修改实际 DOM。对于已经习惯手工寻找节点、绑定事件、精确更新属性的开发者,这听起来差不多等于:房间里一盏灯坏了,先重画整栋楼的图纸。
React 团队没有否认早期反应糟糕。开源一周年时,官方回顾说外界最初多半持怀疑态度,JSX 尤其筛掉了大批潜在使用者。这里没有必要补写“全场哄笑”之类的传奇场面,史料并不支持。仅凭代码本身已经足够解释那种不适:React 不是给旧前端增加一个顺手工具,它在要求开发者重新划分责任。
过去,程序员负责告诉浏览器“怎样把界面从 A 改成 B”。React 希望程序员只说“状态 B 应该长什么样”,至于怎么走过去,由库负责。过去,HTML 模板与 JavaScript 逻辑按文件类型分家。React 则把同一个界面的标记和行为放在一起,把边界画在彼此独立的组件之间。
这才是 React 历史里最重要的转向。
很多回顾把它写成虚拟 DOM 的胜利史,好像 React 靠一项巧妙的性能优化击败了其他框架。这个说法太省事。虚拟 DOM 从来不是一颗可以单独出售的魔法药丸,React 也没有凭空发明“先保留中间表示、再计算更新”这种思想。对行业产生持久影响的,是组件、声明式渲染、单向数据、协调算法和渐进采用共同组成的一套工作方法。
此后的十几年,React 一直在重复同一个动作:先把某类复杂性收进内部,再为这份控制权支付新的代价。它接管 DOM 更新,于是要维护协调算法;它接管渲染时机,于是要重写调度器;它进入服务器,于是要面对序列化协议和远程代码执行漏洞;它走进编译器,于是要静态理解 JavaScript 的数据流;它成为多家公司共同依赖的基础设施,于是连所有权和治理都不能继续只放在 Meta 手里。
React 的发展史,说白了,是一套 UI 编程模型不断扩大责任边界的历史。
| 时间 | 关键节点 | React 新接管的责任 |
|---|---|---|
| 2013 | React 与 JSX 开源 | 用声明式组件协调 DOM 更新 |
| 2014 | Flux | 明确大型应用的数据流方向 |
| 2015 | React Native、0.14 分包 | 把组件模型带离浏览器 |
| 2016 | Create React App | 把构建配置做成可升级产品 |
| 2017 | Fiber、React 16 | 管理渲染工作的优先级与中断 |
| 2019 | Hooks | 让状态逻辑以函数方式组合 |
| 2022 | React 18 | 把并发、流式 SSR 推向稳定版 |
| 2024 | React 19 | 协调表单 Action 与服务器边界 |
| 2025 | React Compiler 1.0 | 在构建期分析并优化组件 |
| 2026 | React Foundation | 把项目资源治理移出单一公司 |
React 出现以前,前端到底在忙什么
要理解 React 为什么显得离经叛道,得先回到 2010 年前后的浏览器。
那时 jQuery 已经把跨浏览器 DOM 操作磨得相当顺手。选择一个节点,改 class,塞一段 HTML,发起 Ajax,请求完成后再更新列表,这些事不再需要开发者亲自和每个浏览器的脾气搏斗。对于内容页、小型后台和局部交互,jQuery 是一把漂亮的瑞士军刀。
麻烦出在页面开始像应用。
一条动态可能同时影响列表、未读数、导航提醒、详情面板和推荐区域。用户在一个地方点了赞,若干视图都要跟着变化。请求可能先回来,也可能后回来;某个弹窗还保留着旧数据;一个事件处理器更新模型,模型又触发其他监听器,新的监听器继续修改界面。程序能运行,不代表谁还能说清一次点击究竟会经过多少条路径。
Backbone 给这种混乱提供了 Models、Collections、Views 和事件。AngularJS 用模板、指令、作用域与双向绑定减轻手工同步。Ember 更进一步,愿意用约定和完整框架换取一致的工程结构。它们都在解决真实问题,也都影响了 React 后来的生态。不能因为 React 后来流行,就把此前的工具写成一群等着被淘汰的笨办法。
分歧落在责任分配上:状态改变以后,谁负责维持界面一致?
命令式代码把责任交给开发者。你知道哪个值变了,也要知道它对应哪些节点,按怎样的顺序修改才不会露出中间态。双向绑定让框架替你传播变化,写起来轻快,可依赖关系一多,更新会沿着观察者网络扩散。单个绑定很好懂,几十个模型、模板和监听器互相牵动时,因果链又会躲起来。
React 选择了一个在当时相当奢侈的答案:别维护那张越来越难画的更新路线图。给定当前 props 和 state,组件重新描述界面。库保存上一次描述,算出差异,再把必要修改提交给宿主环境。开发者不再逐条维护“变化传播网络”,而是反复回答一个更稳定的问题——现在的界面应该是什么。
这种答案当然没有消灭复杂性。它只是把复杂性搬家了。
原先分散在业务代码中的节点查找、更新顺序和事件维护,被搬进 React 的 renderer 与 reconciler。开发者获得更直接的心智模型,React 团队则接手一台越来越复杂的机器。日后 Fiber、并发渲染、Suspense 乃至 Server Components,都从这次责任转移里长出来。
双向绑定不是反派,规模才是试纸
站在今天嘲笑 AngularJS 的脏检查或 Backbone 的事件链并不公平。Knockout、AngularJS 与 Ember 让大量团队第一次能把数据变化自动映射到界面,少写了成片浏览器兼容代码。小表单里,输入框和模型同步更新非常直觉;内容后台里,模板比手工拼 DOM 清楚太多。React 团队自己也是在使用 BoltJS 多年后,才从大型广告应用里确认问题。
差别在规模放大后出现。双向绑定常把依赖建立在运行时:模板读取变量,观察系统发现变化,再通知别处。开发者不必提前声明完整更新图,这是便利;调试时想知道“谁改了这个值”,同一份便利又变成遮挡。Backbone 的显式事件更可控,事件名和订阅者一多,也会形成看不见的总线。
React 用每次 render 重新读取数据,减少长期存活的点对点绑定。更新来源更集中,界面结果从当前状态推导。它付出的成本是更多重新计算、不可变数据习惯和向下传 props。AngularJS 让框架观察变化,React 要求应用明确状态所有权。很难说谁天然更先进,区别在于维护费最后落到谁的桌上。
后来 Vue 同时保留模板与响应式依赖追踪,Svelte 在编译期生成定点更新,Signals 又让细粒度订阅复兴,都说明业界没有在 2013 年找到唯一答案。React 赢得的是一种足够稳固、又能被普通 JavaScript 表达的折中。它在 Facebook 的规模下经受住考验,其他公司因此愿意相信自己也能从中获益。
Facebook 的问题不是“不会写 JavaScript”
React 不是实验室里凭空设计的一套纯洁理论,它出自 Facebook 广告业务。
官方在 2016 年做过一次源码考古。早期广告团队使用名为 BoltJS 的内部 MVC 框架,其中已经出现 render、createClass 和 refs 等后来能在 React 里看到的概念。Bolt 并非毫无章法,它允许视图保存对子节点的引用,再在数据变化时定点修改内容。放在一个组件里看,做法挺合理;应用长大后,代码开始变得难以追踪。
广告产品尤其容易把前端逼成小型操作系统。受众条件、预算、出价、素材、预览、审核状态、统计数字和错误提示同时出现在屏幕上。某个输入改变,不只改一行字,它会使别的选项失效、重新计算预估、刷新图表,甚至触发后台请求。每项业务规则都说得过去,规则连在一起就成了迷宫。
这类问题常被轻率归结为“工程师水平不够”。其实吧,越有经验的团队越容易撞上它,因为他们有能力把更多功能塞进同一页面。工具在小规模下隐藏的耦合,会在产品成功之后集体收债。
Jordan Walke 没把麻烦归到某个写坏的函数上,他怀疑的是整套更新模型。他开始做一个业余实验 FaxJS。名字后来消失了,里面却已经有 props、state、组件、大范围重新计算后比较界面、服务端渲染等 React 的骨架。其核心直觉带着明显的函数式倾向:界面最好能由数据推导出来,能少修改就少修改。
2012 年 3 月,Walke 把 FaxJS 带进 Facebook 代码库,改名 FBolt。这个时间点值得说准。许多二手时间线把“2011 年用于 News Feed”当成标准答案,React 官方源码考古给出的可核日期却是 2012 年 3 月入库。与其为了年表整齐复制一个流行数字,不如承认内部原型、试用和正式进入代码库很难压成一天。
FBolt 的技术想法很激进,进入组织的姿势却很保守。它可以和旧 Bolt 互操作,团队能够挑一个适合函数式表达的组件换掉,再观察结果。失败了,损失局部;有效,再往外扩。大公司内部的新技术若要求一口气改写全部产品,通常死在证明自己之前。React 从一开始就学会了“允许只用一点点”。

后来 Jordan Walke 与 Tom Occhino 把 FBolt 改名 React。Walke 当时解释,很多系统所谓“响应式”仍要求开发者建立点对点监听,而这个 API 能对任意形态的 state 或 props 变化作出反应。名字没有夸耀速度,它描述的是职责:数据变了,系统负责让界面跟上。
XHP、FaxJS 与一条被误解的血缘
JSX 最容易被看成一个大胆的审美决定:Facebook 工程师忽然觉得在 JavaScript 里写 HTML 很酷。真实来路更务实。
Facebook 自约 2010 年起就在 PHP 中使用 XHP。XHP 允许工程师把 XML 字面量直接写进 PHP,并把自定义标签变成有类型、有行为的组件。它最初有安全动机,可以减少手工拼接字符串带来的 XSS 风险,后来又证明,把标记与负责生成它的逻辑放在一起,适合组织复杂界面。
在 React 进入 Facebook 代码库前,Adam Hupp 已做过一个用 Haskell 编写的 JavaScript 转换原型,让 XML 风格语法编译成普通函数调用。早期讨论一度纠缠于 innerHTML 与 document.createElement 谁更快。以当年的 IE8 和 Firefox 看,这不是无聊的微优化:选择不同生成方式,可能真会决定界面能不能接受。
Walke 的中间表示绕开了这场争论。JSX 不必直接创建 DOM 节点,它只要把标签变成嵌套函数调用,React 再决定如何渲染。于是语法层和宿主环境脱开:同一份组件描述可以变成浏览器节点,可以在服务器上生成 HTML,后来还可以变成 iOS 与 Android 的原生视图。
React 最终没有直接使用 Hupp 的实现,而是 fork 了 XHP 创建者 Marcel Laverdet 的 js-xml-literal 项目。JSX 这个名字也从那里延续下来。JSX 没有把 HTML 偷偷藏进 JavaScript;它为深层嵌套的函数调用提供了一套更容易阅读的写法。
这一区别解释了为什么“关注点分离”争论多年不散。
反对者认为,结构、样式和逻辑混在一个组件文件中,破坏了 Web 原有的清晰边界。React 的回答是:按技术类型分文件不等于按业务责任分离。一个点赞按钮的标记、交互状态和事件逻辑会一起变化,把它们拆到三个全局文件里,只是物理分开,维护者仍要同时打开。组件把共同变化的东西收在一处,再用 props 与外部世界连接。
这个观点有用,但不能被当成教条。组件文件可以膨胀,CSS、请求、权限和分析埋点也会重新纠缠。React 只改变了默认边界,没有自动替团队完成设计。JSX 活下来,靠的并非“混合永远优于分离”这种口号;它让什么东西应该一起变化成为可讨论的工程问题。
组件不是自定义标签,是责任边界
今天说“前端组件化”太容易了,按钮、弹窗、头像都能叫组件。回到 React 刚出现时,它更激进的地方是把状态、生命周期和界面描述收进同一个可组合单元。
一个组件接收 props,拥有或不拥有自己的 state,返回一棵元素树。父组件通过数据决定子组件看到什么,子组件通过回调报告发生了什么。这样的接口看起来像普通函数调用,却把界面中最难管的两个问题绑在一起:数据归谁,更新归谁。
一个成熟组件至少要把几类责任说清楚:
- 组件输入:哪些 props 是稳定合同,哪些值应由上层拥有;
- 局部状态:哪些变化只属于局部,生命周期跟谁一致;
- 行为输出:用户行为通过什么事件或回调向外报告;
- 副作用:哪些工作必须和浏览器、网络或第三方系统同步;
- 故障边界:异常、加载和空状态由哪一层接住。
这是一种组织代码的方式,也是一种组织团队的方式。
在旧式页面脚本里,任何功能都可能通过选择器摸到任何节点。约定写在文档里,浏览器不会阻止支付模块顺手改掉导航栏。组件树让边界可见:要影响另一个分支,通常得提升状态、传递 props、进入共享 store,或者明确使用 Context。路径变长了,却留下了因果痕迹。
React Native 官方后来总结,组件让工程师无需把整个系统都装进脑子,便能改动一个局部。这句话比“复用按钮”更接近组件化的商业价值。几行 DOM 花不了大公司多少钱;昂贵的是一次改动需要多少人互相确认,又可能碰坏多少隐含规则。
组件也带来新的治理难题。粒度太粗,一个组件拥有半个页面的状态;粒度太细,props 像快递一样层层中转。状态放低了,两处需要同步;放高了,每个细小变化都惊动大树。后来 Flux、Redux、Context、Hooks 和各种服务器状态库轮番流行,实质都在回答组件模型故意没有包办的问题:跨边界的数据到底放哪儿。
React 的框架克制为生态留下了空间,也把选择成本留给了使用者。
“状态放哪儿”成了新的前端基本功
组件模型普及后,前端面试和代码评审里多了一个此前不那么显眼的问题:谁拥有这份状态?
输入框文字只服务当前组件,可以留在本地。两个兄弟组件都需要同一选择结果,状态通常提升到最近共同父级,再通过 props 传下去。值由 React state 决定的输入叫受控组件,浏览器自身保存值的叫非受控组件。选择看起来只是 API 偏好,实际决定校验、重置、性能和第三方表单库怎样介入。
“状态提升”能恢复单一事实来源,也会让上层组件变成交通枢纽。十层组件只为把主题传到底部,就是 prop drilling。Context 为跨层共享提供通道,但它不是免费全局变量:Provider 的值变化会影响订阅者,边界过大时,局部更新又可能扩散。Redux、MobX、Zustand、Jotai 以及各种服务器状态库,正是在不同粒度和更新模型之间重新报价。
React 从未替开发者定义完整领域模型。它只通过数据向下、事件向上的默认方向,把所有权问题逼到明面。坏消息是架构讨论变多了;好消息是团队终于不得不说清楚,一份值为何存在、由谁修改、生命周期跟哪个界面一致。
这也是 React 组件比“可复用标签”更深的影响。它让状态归属从实现细节变成设计语言。组件难以拆分,往往因为几类生命周期不同的数据被塞进了同一责任边界,而非单纯因为 JSX 太长。
重新渲染,不等于重新造一遍页面
React 早期最成功、也最容易误导人的宣传词是 Virtual DOM。
它抓住了开发者的具体恐惧:实际 DOM 操作相对昂贵,如果每次数据变化都“重新渲染”,页面岂不是要卡死?React 的回答是,render 返回 DOM 应有形态的轻量描述,而非字符串或 DOM 节点。系统比较前后描述,生成一组最小的实际修改。这个过程叫 reconciliation。
2013 年官方文章拿 TodoMVC 举例,称一次重新渲染约一毫秒。这个数字只能被当成当时那个示例和环境下的官方基准,不能翻译成“React 所有页面更新都只需一毫秒”。真实成本取决于组件数量、计算、浏览器布局、设备和写法。历史文章喜欢把旧基准升级成永恒定律,技术营销就是这样悄悄变成神话的。
| 更新模型 | 开发者主要描述什么 | 系统承担什么 | 常见代价 |
|---|---|---|---|
| 命令式 DOM | 哪个节点怎样变化 | 执行具体 DOM API | 业务代码长期维护更新顺序 |
| 双向绑定 | 数据与模板的绑定关系 | 观察并传播依赖变化 | 大规模时因果链可能隐藏 |
| React 协调 | 当前状态对应的元素树 | 比较、调度并提交修改 | 运行时协调与身份管理成本 |
| 编译型更新 | 声明式模板或可分析组件 | 构建期生成定点更新 | 表达自由受编译器能力约束 |
| Signals | 细粒度可响应值 | 精确通知读取者 | 依赖粒度与生命周期需治理 |
更关键的是,React 的价值并不要求虚拟树比较在任何场景都比手工更新快。一个知道确切节点的优秀程序员,当然可以写出更少的操作。问题是他还得保证未来每个功能、每条异步路径和每次团队交接都维持这种精确。React 用一部分通用计算,换取业务代码不必长期保存手工更新图。
这是一笔工程经济账,不是百米赛跑。
协调也不是“逐像素寻找绝对最小差异”。React 依赖启发式规则:元素类型变化通常意味着替换子树;列表中的 key 帮助识别项目身份。key 写错时,输入状态会跑到错误行,动画会奇怪,组件会被意外重建。抽象没有消除身份问题,只是要求开发者用更明确的方式表达身份。
后来 Svelte 等编译型框架会追问:既然很多更新关系能在构建时分析,为什么一定要在运行时比较树?Signals 路线又追问:能否追踪更细粒度的依赖,直接通知使用者?这些批评并未证明 React 一开始选错了,它们提醒我们,声明式 UI 有多种实现价格。React 选择通用 JavaScript、运行时协调与渐进采用,换来了表达自由,也长期背着运行时与调度复杂度。
事件系统把浏览器差异也收进了合同
早期 React 还承诺一件今天不太显眼的事:事件在不同浏览器里表现一致,连 IE8 也尽量提供统一接口。它在顶层做事件委托,把原生事件包装成 SyntheticEvent,再按组件树分发。组件作者写 onClick,不必同时处理几套绑定 API 与属性名。
这和 Virtual DOM 是同一种交易。React 不只接管节点更新,也接管事件进入应用的通道。它可以批处理事件里的 state 更新,统一冒泡行为,把浏览器兼容放进 renderer。开发者获得稳定合同,少数与原生事件不同的细节则会令人困惑,例如早期事件对象复用后异步读取失效。
React 17 为渐进升级把委托位置从 document 移到根容器,并取消事件池。看起来不起眼,却说明抽象层要不断适应浏览器本身的进步。2013 年必须抹平的差异,几年后可能已经不存在;曾经合理的全局委托,又会妨碍多个版本共存。
表单让这份合同更深入。受控输入把浏览器内部值重新交给 React state 管理,使验证、联动和重置可预测,也可能让每次按键触发组件更新。大型表单库后来会使用非受控节点、字段级订阅等方式减少成本。React 给出了统一模型,生态继续为不同规模寻找更便宜的实现。
这些细节很少出现在“React 改变前端”的宏大叙事里,实际迁移项目时却极其重要。开发者愿意从 jQuery 转向组件,不只因为思想新,还因为日常兼容工作真的少了。
Instagram 把内部工具推向了公共世界
一项公司内部技术要开源,填上许可证只是第一步,更难的是拆掉它对内部环境的隐性依赖。
2012 年 Facebook 收购 Instagram。Pete Hunt 原在 Facebook 照片与视频团队,随后加入 Instagram 新成立的 Web 团队。他想用 React 完整构建网站,这和 Facebook 内部逐个组件替换的保守路线很不一样。Instagram 不使用 Facebook 那套内部基础设施,React 若要在那里运行,就必须清理依赖、明确公共 API、拥有外部可用的构建方式。
压力反而帮了 React。
Hunt 推动 React 与 Facebook 私有环境解耦,发现并推广了当时还很年轻的 webpack,也实现了服务端渲染所需的 renderToString。设计师 Maykel Loomans 为网站做的视觉稿又确定了早期 React 网站的蓝色与原子轨道标志。一个内部实验开始拥有文档、构建、品牌和外部用户能理解的边界。
Instagram 对 React 的帮助不只是一份大公司背书,它还证明这套模型可以脱离发明它的组织运行。很多大公司项目开源后难以使用,代码质量未必差,问题常在于构建系统、权限服务、数据层和运维习惯被当成了空气。外部团队拿到仓库,才发现缺的是整座城市。Instagram 像一次提前进行的外部部署演习。
2013 年 5 月 29 日 React 开源后,Pete Hunt 又承担了大量解释工作。官方在 6 月 5 日发表《Why did we build React?》,开门见山:React 不是 MVC 框架;它是构建可组合用户界面的库;它不使用传统模板;JSX 只是可选语法。
“React isn’t an MVC framework.”
— Pete Hunt,2013 年《Why did we build React?》;React 官方博客
这些否定句很有意思。新项目通常急着列功能,React 却先说明自己不负责什么。它面对的是一个已经被 MVC、模板和双向绑定占满的概念市场,只有先拆开旧坐标系,用户才知道该把 React 放在哪里。
一年后,React 官方统计约 2300 次提交、1500 个 issue 与 pull request、接近 7000 个 GitHub star,Khan Academy、《纽约时报》和 Airbnb 等已在生产中使用。数字不算今天意义上的爆炸,却足以说明最初的排斥没有阻止第二批开发者进入。有人看中性能,有人看中 ClojureScript 等函数式语言与 React 的契合,也有人只是厌倦了追踪双向更新。
React 没有在发布当天征服前端。它靠一批愿意越过 JSX 第一印象的人,慢慢获得了可见的反例:原来这东西真的能用。
一套新语法,需要一整条工具链替它翻译
JSX 能普及,靠的不只是观念说服。浏览器不直接认识它,开发者需要转换器、source map、错误定位、编辑器高亮和构建工具。React 早期自带 JSXTransformer.js,可以在浏览器现场编译,适合演示,不适合生产。随后 JSX 进入 Babel,模块交给 webpack 组合,热更新让组件改动不刷新整页也能看到。
Pete Hunt 在 Instagram 解耦 React 时推广 webpack,不是一个无关花絮。组件把标记、逻辑和依赖收在模块里,打包器则负责从模块图产出浏览器可加载的资源,两种模型恰好互相加强。CSS Modules、代码分割和各类 loader 随后也开始围绕组件边界组织资源。
React DevTools 把组件树、props、state 和性能信息放进浏览器扩展。没有它,开发者只能看最终 DOM,很难知道某个节点由哪个组件生成。抽象层只要遮住底层,就必须提供新的观察窗。DevTools 不是配件,是 React 心智模型能够调试的条件。
工具繁荣也制造门槛。一个“只是 UI 库”的项目,安装后却要学 npm、Babel、webpack、ESLint 和几十项配置。React 的语言自由让生态能快速试验,官方没有统一工具又导致教程碎片化。CRA 后来会填这个坑;CRA 退场后,Vite 和框架再次接手。围绕 JSX 的工具史像一条摆动的钟:先分散创新,再集中默认值,默认值老化后重新分散。
React 如何随工程经验进入下一家公司
开源项目流行往往被画成下载量曲线,组织里的传播更像人员迁徙。工程师在一家公司学会组件、props、Redux 和测试,跳槽后能把同一套词带到下一家公司;组件库作者把日期选择器和表格发布到 npm,陌生团队不必从底层重做;招聘描述写上 React,又吸引更多人优先学习。
这形成了一种人力网络效应。React API 不必在每项指标上最好,只要它周围有足够多现成组件、问题答案、培训材料和有经验的同事,采用风险就会下降。Angular、Vue 也拥有各自网络,React 又因 Facebook、Instagram 和 React Native 的品牌获得早期信用。
设计系统把这种经验固定进组织。按钮不再是一段 CSS,而是带可访问性、交互状态、主题和测试的组件;产品团队组合已验证部件,平台团队维护版本与迁移。React 的 props 与组合模型很适合这类协作,Storybook 等工具又提供独立展示环境。组件复用因此从“少写几行”变成统一产品行为。
网络效应也会制造保守。公司积累几千个组件后,即使新框架在局部更快,迁移培训、组件库、监控与招聘的总成本仍可能更高。React 的地位逐渐不只由代码质量保护,还由沉没资产保护。新版本必须兼容,核心团队的选择又会影响大量无法迅速转向的组织。
React 当然也会衰落,只是框架竞争进入成熟阶段以后,胜负取决于整套迁移经济。技术社区喜欢比较语法能少写几行,企业计算的却是要花几年。
单向数据流:预测性是拿样板代码换来的
React 让组件从 props 和 state 推导界面,却没有替大型应用安排所有数据。多个组件需要同一份未读数怎么办?请求结果存在哪儿?一个动作要同时更新几个业务域,谁决定顺序?
Facebook 的回答是 Flux。
2014 年公开的 Flux 不是一个严密框架,更像一套交通规则。View 产生 action,dispatcher 把 action 送给 store,store 更新后通知 view。数据沿一个方向流动,不鼓励模型之间随意互相触发。官方当时直说,他们发现双向绑定会导致级联更新:一个模型改变另一个模型,单次交互究竟造成什么越来越难预测。
Dispatcher 单例只是 Flux 的外形。更长久的贡献,是把“发生了什么”和“界面怎么变”分开。Action 描述事件,store 负责业务状态,React 负责把结果画出来。调试者可以沿着同一个方向追踪,不必在观察者网络里来回游。
代价马上出现了。Action type、action creator、dispatcher、多个 store、订阅和清理,一个简单按钮也可能带上一串仪式。Facebook 又强调 Flux 是模式而非正式框架,社区只好自己决定每个细节。很快冒出大量 Flux 实现,各自宣称更简单、更纯、更适合异步。
这段历史暴露了 React 的典型生态机制:核心团队把问题边界说清,却不立即垄断答案;社区竞争填满空白;一种方案胜出后,又反过来塑造大家对 React 的理解。
从自由角度看,这很繁荣。从团队采购角度看,多少有点折磨。选 React 不再只是选一个视图库,还要选状态方案、路由、请求库、构建工具和测试组合。React 的“小”常常把体积转移到决策清单里。
Redux:一个演讲原型怎样成了默认行李
2015 年中,Dan Abramov 为会议演示制作一个 Flux 风格的状态库。他想展示时间旅行调试:用户执行一串操作后,开发者能在历史状态之间前后跳转,看每个 action 怎样改变应用。要做到这一点,状态变化必须可重复,旧状态不能被随手改坏。
Redux 把多个 store 收成一棵状态树,用 action 描述事件,用 reducer 根据旧状态和 action 计算新状态。Andrew Clark 等人在早期设计中参与推进。它吸收 Flux 的单向流,也受到 Elm、函数式 reducer 思路和 hot reloading 实验影响。结果是一套小得出奇的核心 API。
小核心不等于少代码。
早期 Redux 项目常见 actions/todos.js、constants/todos.js、reducers/todos.js 三件套。加一个待办事项,要先定义字符串常量,再写 action creator,再在 reducer 的 switch 中处理,异步请求还要装 thunk 或 saga。不可变更新一层套一层,漏一个展开运算符就可能直接修改旧状态。Redux 官方后来也承认,这些模式样板繁重,而且许多人在根本不需要 Redux 的地方使用了它。
它为什么仍然迅速流行?因为那时 React 的旧 Context API 很难可靠传递持续变化的数据。大型应用确实需要在组件树外组织共享状态,Flux 实现又多到让人疲惫。Redux 给出一个可测试、可记录、可接 DevTools 的共同语言。到 2016 年,社区甚至形成“用了 React 就得用 Redux”的误解。
这是一种很常见的技术宿命:配套工具一旦替核心平台补上关键缺口,就会从“可选”变成“标准答案”,然后被带去解决它不擅长的问题。
2017 年以后,新的 Context API、Apollo、React Query、SWR 等数据获取与缓存工具把问题重新拆开。服务器数据并不等于客户端业务状态,表单局部状态也不必塞进全局 store。2019 年 Redux Toolkit 又用 configureStore、createSlice 和 Immer 减少旧式样板。Redux 没有消失,它从万能行李箱退回一件有明确使用场景的工具。
对 React 史来说,Redux 的影响甚至超出自身。它让 action、reducer、不可变更新、时间旅行和单向数据流进入普通前端团队的词汇,也证明 React 留白的地方会长出比核心库更强势的生态规范。
Relay 与 GraphQL:组件还应该声明自己需要什么数据
Redux 解决状态如何变化,Facebook 同期还在处理另一件事:组件需要的数据怎样从服务器来。
2015 年初,Facebook 在首届 React.js Conf 介绍 Relay 与 GraphQL。Relay 让组件在旁边声明自己的数据依赖,框架把整棵组件树的查询组合、批处理和去重,再把结果以 props 交给组件。GraphQL 提供描述嵌套数据的查询语言与可验证 schema。两者都已在 Facebook 生产环境使用,随后才逐步开源。
这个设计把组件的“自包含”往后端推了一步。过去页面入口先请求一个巨大 payload,再把字段层层传下。子组件搬到别处,父级请求和服务端 endpoint 也要跟着改。Relay 希望组件声明自己精确需要的字段,父组件组合子组件时,数据需求一同组合。
官方当时强调,静态查询可提前抽取与校验,框架能合并重复请求、跟踪哪些组件使用哪些记录、处理分页和乐观更新。它还区分 Relay 管理的服务器数据与 Flux 管理的局部应用状态。后来“服务器状态不等于客户端状态”成为 React Query 等工具的重要主张,源头问题在这里已经很清楚。
Relay 没有像 Redux 那样成为每个 React 初学者的默认行李。它对 schema、编译与后端配合要求更高,GraphQL 自身反而越过 React 成为独立生态。但 Relay 留下的方向深刻影响了 Suspense 与 Server Components:让数据依赖靠近组件,再由框架统一安排请求,而不是每个组件在 Effect 里各发一枪。
从 Flux 到 Relay,Facebook 在做同一类组织工程。产品团队人数增加后,局部开发者应当能声明需求,系统负责把全局代价合并。React 组件不只是一块视图,它逐渐成为数据、代码与加载边界的汇合点。
React 从一个库长成了一套工作方式
2015 年 10 月,React 0.14 把原来的单一包拆成 react 和 react-dom。表面看只是 import 路径变化,官方解释却很重:观察 React Native、React ART、react-canvas、react-three 等项目后,他们越来越确定,React 的美和本质与浏览器或 DOM 无关。
“The beauty and essence of React has nothing to do with browsers or the DOM.”
— Sophie Alpert,React 0.14 发布说明;React 官方博客
react 保留元素、组件类和组合所需的通用能力;负责把树挂进浏览器、查找 DOM 节点、在服务器生成 HTML 的 API 则进入 react-dom。这次拆分把早期 JSX 的潜台词写进包结构:组件描述和最终渲染目标是两回事。
从架构上说,React 开始出现清晰的三层。组件作者声明元素;reconciler 决定树怎样协调;renderer 把结果提交到 DOM、原生移动端或别的宿主。普通开发者未必需要知道这些名词,生态却因此获得了扩展点。React 不再只是“比 jQuery 更方便地改网页”,它试图成为一套跨渲染环境的 UI 内核。
0.14 还正式引入无状态函数组件。那时函数组件只能接收 props 并返回元素,没有实例,没有生命周期,也没有 state。官方把它定位成更简单的组件写法,并预告未来能为它做专门优化。四年后的 Hooks 会把函数组件从轻量配角推到舞台中央。
版本迁移的处理方式也值得注意。Facebook 当时已有超过 1.5 万个 React 组件。0.14 的大改动先给警告,旧名称继续工作到下一版本;官方还提供自己在 Facebook 使用的 codemod。React 团队很早就明白,大规模采用之后,API 设计只完成一半,迁移路径才决定新版本能否落地。
兼容性从此成为 React 的制度约束。一个年轻框架可以说“重写更干净”,基础设施不行。
从运行时警告到类型检查,组件接口越来越正式
早期 React 用 PropTypes 在开发环境检查 props。组件可以声明字段应是字符串、函数还是必填项,传错时控制台报警。它不像静态类型那样在构建前阻止错误,却让“组件期待什么”第一次能与实现放在一起。
随着生态成熟,接口检查逐渐从 React 核心移出。React 15.5 把 React.PropTypes 和 React.createClass 分别迁到独立包,核心继续收缩。Facebook 内部长期使用 Flow,外部 TypeScript 社区则为 React 维护类型定义。函数组件、泛型组件、事件与 ref 的类型不断变细,组件 props 越来越像一份可由编辑器理解的公共合同。
类型系统没有替代运行时验证。服务器传来的 JSON 不会因为 TypeScript 声明就自动可信,跨包版本也可能让类型与实际行为错位。它最有价值的地方,是在重构组件树时追踪接口变化:改一个 prop 名称,编译器能找出大量调用者;Context、reducer 与 Hook 返回值也能留下结构。
React Compiler 以后更进一步,静态分析不只检查字段形状,还要理解数据流、纯度和可变性。2013 年 React 把 HTML 风格标记引进 JavaScript,2025 年编译器进入稳定版,把 JavaScript 当成可优化的 React 程序。自由语言与更强工具并不是反方向,React 的路线一直是先允许普通 JavaScript 表达,再逐步用开发工具把隐含合同照亮。

React Native:同一种想法,两个完全不同的屏幕
React Native 常被介绍成 React 成功后的横向扩张,其实种子在 2013 年已经露出来。Pete Hunt 的早期文章提到,Facebook 做过通过 Objective-C bridge 驱动 iOS 原生视图的内部原型。轻量界面描述既然不等于 DOM,就没有理论要求它只能变成 div 和 span。
推动 React Native 走出原型的,是 Facebook 当时的移动困境。
Web 开发保存文件、刷新页面就能看到结果,Facebook 网站当时一天甚至可以发布两次。原生 iOS 与 Android 项目需要编译,平台 UI、手势、导航和线程模型又完全不同。业务团队被分成几套技术栈,实验从几小时反馈变成等待应用版本发布。公司口号里写着 “Move fast”,手机编译器可不认识这句话。
用 WebView 包一层能保留 Web 的迭代速度,却难以获得原生控件的手感与平台一致性。用 Objective-C 重写 React 思路的 ComponentKit 能得到原生性能,工程师仍得学习另一种语言和工具链。React Native 选择第三条路:JavaScript 在独立环境执行,用 React 描述界面,通过异步消息与批处理驱动 UIKit 或 Android 原生组件。
2015 年 3 月 F8,React Native for iOS 开源;同年 9 月 Android 版发布。Facebook Ads Manager 成为早期完整使用 React Native 的跨平台应用,Groups 则采用原生代码与 React Native 混合的渐进路线。
官方特意拒绝了最诱人的口号 “write once, run anywhere”。iOS 和 Android 有不同控件、手势、导航习惯与能力,假装它们完全相同,往往只能做出两边都别扭的界面。React Native 的说法是 learn once, write anywhere:工程师复用组件思想和 JavaScript 技能,但仍为每个平台做适配。
“We call this approach ‘learn once, write anywhere.’”
— React Native 团队,2015 年开源说明;Meta Engineering

这句区别后来被大量营销文案抹平。跨端项目总被追问“代码复用率多少”,仿佛共享行数越高越成功。可跨端带来的主要节省常在状态模型、数据层、调试方式、人员训练和发布工具,不在某个按钮文件是否共用。为了追求百分之百复用而强行抽象平台差异,维护成本反会上升。
React Native 也不是把 Web 页面自动翻译成原生应用。早期 bridge 的往返成本、线程调度、列表性能和原生模块接入都可能成为瓶颈,团队仍需理解移动平台。后来新架构引入 Fabric renderer、TurboModules 和 JSI,正说明最初的抽象需要随着规模重做底层通道。
但 React Native 改变了一件更长久的事:React 组件模型被证明可以脱离浏览器。0.14 分包不再像洁癖,reconciler 与 renderer 的分层成了产品能力。此后谈 React,已经不能只把它放在“网页模板库”那一格。
Renderer 生态证明了抽象,也暴露了抽象成本
核心与 DOM 分离后,社区把 React 用在 Canvas、WebGL、PDF、命令行甚至三维场景。React Three Fiber 让开发者用组件描述 Three.js 对象,React PDF 把组件树转成文档结构,测试 renderer 可以在没有浏览器的环境检查输出。它们共享的不是像素,而是元素、组件、状态更新与协调协议。
这类 renderer 证明 React 的抽象确实越过 DOM。它们也提醒人们,跨宿主不是白送的。每种环境都有自己的属性、事件、布局和资源生命周期。浏览器移除一个节点很简单,三维场景可能要释放纹理与缓冲区;移动端文本嵌套规则也不同于 HTML。Renderer 作者必须把 React 的提交阶段翻译成宿主语义。
生态因此形成两类复用。业务逻辑、自定义 Hook 和状态模型可以跨平台共享,最靠近视觉与交互的组件常要分开实现。把 renderer 当成“自动适配器”会失望,把它当成“共同调度语言”更准确。
React 自己也受到这些非 DOM 环境反向约束。核心变更不能只在 Chrome 里正确,还要考虑 Native renderer 和第三方实现。抽象一旦对外开放,就会变成兼容承诺;成功的扩展点会限制未来重构。React 16 的 Fiber、后来的并发能力和 React Native 新架构,都得在共享内核与宿主特性之间反复校准。
Create React App:零配置的黄金年代
React 核心愿意保持小,入门体验却越来越大。
到 2016 年,写 JSX 往往意味着安装 Babel,模块化意味着 webpack,开发时需要服务器与热更新,代码质量要接 ESLint,生产构建还要压缩、替换环境变量、给文件名加 hash。每件工具都合理,组合起来却像一场资格考试。教程刚写完,插件版本已经不兼容。
社区把这种疲惫叫 JavaScript fatigue。Create React App 的发布文章引用了一句尖锐概括:选择 React 与 JSX,像是不知不觉选择了构建工具、样板、linter 和时间黑洞。对于只想写一个 Hello World 的人,先理解 loader 顺序实在有点过分。
2016 年 7 月,Dan Abramov 与 Christopher Chedeau 在一次 hackathon 中做出 Create React App 原型,Kevin 等协作者随后加入。它把 webpack、Babel、ESLint 和开发服务器收进 react-scripts,新项目只看到一项构建依赖和几个命令。没有配置文件,没有复杂目录;生产构建带压缩与缓存文件名。
谈不上技术突破,它把决策做成了产品。
CRA 替初学者和普通团队挑好一组兼容版本。升级一个依赖,就能拿到新的 Babel、webpack 和 lint 规则。配置封装不仅减少敲键盘,还减少“我是否选错了”的背景焦虑。Ember CLI 和 Elm Reactor 已展示过这种价值,React 终于补上自己的统一入口。
它还准备了 eject。如果默认配置不够,运行一次命令,所有依赖与脚本都会展开到项目里,此后由团队自己维护。名字很诚实:弹射座椅只能用一次。你获得全部控制,也离开官方升级轨道。
CRA 的诞生也打破了 React 团队此前的一条开源习惯:只发布 Facebook 内部投入生产的东西。Facebook 自己拥有强大的远程开发与构建基础设施,外部小团队的问题在公司内部并不存在。要改善公共生态,只能专门为外部用户做一个 Facebook 不用的项目。
这件事提醒人们,开源治理不能只说“我们把内部好东西送出来”。当一个项目拥有巨大外部社区,维护者还得照顾自己没有的使用环境。CRA 一度是 React 最成功的公共服务之一。
零配置为什么后来变成了零维护
封装复杂性有个前提:封装层必须持续有人维护。
CRA 的模型适合 2016 年的单页客户端应用。它能生成 bundle,却不负责路由;能让组件在浏览器里请求数据,却不知道某个路由进入前需要哪些数据;能用 React.lazy 分割代码,却难以让路由、代码和数据提前并行加载。页面规模增长后,开发者发现构建只是应用架构的一小块。
CRA 的代码质量并非症结。Web 应用的性能目标变了。
如果路由组件渲染后才发请求,会形成网络瀑布;如果组件渲染后才发现还要下载另一段代码,用户继续等。要优化这些路径,构建工具需要与路由、数据层、服务端渲染和缓存策略合作。CRA 若要全面解决,就会逐渐变成一个框架。
与此同时,Vite 用原生 ESM 和更轻快的开发服务器重塑了工具体验,Parcel、Rsbuild 等也提供了现代构建入口。Next.js、Remix 和后来框架化的 React Router 把路由、数据加载与服务端能力放到一起。CRA 夹在中间:作为构建工具不再轻快,作为框架又缺少关键层。
2025 年 2 月,React 团队正式弃用 Create React App。公告给出的理由很直接:它没有活跃维护者,现成框架已经在解决那些问题。新建项目会看到弃用警告;旧项目继续以维护模式工作,并有一版兼容 React 19 的发布。官方建议新应用选框架,特殊场景或学习用途可选择 Vite、Parcel、Rsbuild 自己搭建。
从“官方推荐的零配置入口”到“不再推荐”,CRA 走了不到九年。可这不算失败。它把可升级的预设配置变成一个工具类别,今天许多 CLI 和 starter 仍沿用这种模式。它的问题也留下教训:零配置并不代表没有配置,只是有人替你持有配置债务。维护者离场时,债不会一起消失。
更微妙的变化是 React 官方立场。2013 年,React 用“不是 MVC 框架”定义自己;2025 年,它建议多数新生产应用从框架开始。核心包仍是库,推荐路径已经不再假装 UI、路由、数据与服务器能够各自独立优化。
构建工具并没有消失,只是换了责任人
CRA 的故事常被总结成 webpack 老了、Vite 更快。这只讲到表面。2016 年的主要痛苦是工具版本难以拼合,CRA 用一个官方预设解决;2025 年的主要痛苦是路由、数据、代码与服务器时机难以各自优化,单纯换一台更快的 bundler 仍不够。
Vite 适合客户端应用和自定义架构,框架则在 Vite、Rspack、Turbopack 或其他工具之上继续持有配置。使用者看到的配置少了,框架维护者面对的矩阵更大:开发与生产一致性、服务端与客户端模块图、CSS 提取、预加载提示、边缘运行时、缓存失效。所谓“零配置”始终是一种劳动分工。
这对企业选型很现实。小团队也许更喜欢 Vite 的透明与速度,复杂产品愿意用框架约定换取路由和数据协同,组件库则可能只需要最薄的 React 加构建设置。React 2025 年的安装文档终于承认这些项目不是同一种应用,不再让一个 CLI 承担全部入门叙事。
CRA 退场还暴露开源维护的脆弱性。成千上万项目依赖一层配置,不等于有人愿意长期升级它。没有明确所有者、资金和发布节奏,下载量只会扩大风险半径。React Foundation 后来提出资助生态项目,正是因为核心库稳定并不足够;脚手架、路由、测试和编译插件同样决定用户能否安全升级。
许可证危机:代码能用,不等于组织敢用
2017 年,React 最大的一场危机来自一份 PATENTS 文件,与性能回归无关。
React 当时采用 BSD 许可证附加专利授权。企业开源项目增加明确专利条款并非天然恶意,Facebook 的解释是,希望向使用者授予相关专利保护,同时在对方发起特定专利诉讼时终止授权,以减少无意义诉讼成本。站在拥有大量专利、也经常面对诉讼的大公司位置,这套防御逻辑不难理解。
问题在下游。
Apache Software Foundation 把 Facebook BSD+Patents 列入 Category X,认为条款把风险不对称地传给下游使用者,也无法作为 Apache License 2.0 的子集再许可。Apache 项目不能分发这类组件。对普通个人开发者,专利终止条款也许遥远;对基金会、平台公司和需要向成千上万下游用户提供代码的项目,它会进入法务审查。
2017 年 9 月 14 日,WordPress 联合创始人 Matt Mullenweg 公开说,Gutenberg 团队将后退一步,考虑改用其他库重写;基于 React 的 WordPress.com Calypso 也会跟随长期选择。他没有指控 Facebook 必然滥用条款,甚至承认条款比很多公司的模糊做法更清楚。他的顾虑来自 WordPress 核心会下发到超过四分之一的网站:整个生态不该继承一场解释不清的专利争论。
这一下击中了 React 的网络效应。组件库选择不是单个团队的私事,它会传给插件作者、客户、再分发项目和法律部门。技术再好,如果每个采用者都要先说服律师,它就不再是低摩擦基础设施。
9 月 22 日,Facebook 宣布 React、Jest、Flow 与 Immutable.js 将改用 MIT。公告承认,团队没能让社区信服,已有许多团队开始选择替代方案,他们也不指望改证照就自动赢回来。React 15.6.2 在 9 月 25 日切换许可证,第二天发布的 React 16 同样使用 MIT。
把这件事写成“社区一骂,Facebook 良心发现”太简单。更准确的因果是:React 已成为广泛生态的基础,Facebook继续坚持一项对自身有利、却让下游承担审查成本的条款,会削弱这套生态对 Facebook 自己的价值。外部采用越成功,项目所有者越难只按内部风险偏好行事。
许可证不是仓库底部的装饰文字。对基础设施,它也是 API。
开源影响力会反过来约束原来的主人
Facebook 最初能快速推进 React,靠的是集中决策、内部生产验证和愿意长期投入的人。开源后,这些仍是优势:核心团队不必为每次重写先寻找赞助,Facebook 巨大代码库又提供了普通实验室没有的压力测试。
采用者增加,项目却出现另一类用户。Apache 关心可再分发,WordPress 关心数万插件和全球站点,React Native 公司关心移动发布周期,竞争框架关心公共标准。它们的风险模型不再与 Facebook 相同。所有者若只回答内部问题,外部生态会用迁移表达意见。
MIT 换证没有抹掉此前争议,却建立了一个预期:React 的治理决定必须考虑下游网络。后来公开 RFC、工作组、Canary 通道和基金会,都是同一压力的制度化结果。社区声音不会自动等于正确技术方向,封闭决策也很难继续获得基础设施级信任。
这是一种反向依赖。外部公司依赖 React,Meta 也依赖外部公司继续把 React 当成共同平台。生态越大,单方任性的代价越高。React 从公司资产变成行业设施,不只表现为提交者名单变长,更要求决策者开始对从未见过的下游负责。
Fiber:为了将来,先把心脏重写
React 15 以前的协调过程大体是同步递归。一旦开始渲染一棵组件树,JavaScript 会一路做下去,直到完成。页面较小问题不大;树很大、设备很慢,或一次更新伴随昂贵计算时,浏览器主线程会被占住。用户敲字、滚动、点击,也得等这轮工作结束。
这里的难题不只是“算法再快一点”。浏览器里不同工作有不同紧迫度。输入反馈必须尽快,屏幕外列表可以晚一点,已经过时的中间结果干脆不必完成。原来的调用栈不容易暂停和恢复,React 需要把渲染工作变成可管理的单位。
Fiber 就是这次重写。
“Fiber”既指新的协调架构,也指树中可保存工作状态的节点结构。React 可以把大任务切开,记录做到哪里,在合适时机让出主线程,再继续;不同更新可以拥有不同优先级。Andrew Clark 在 React 16 发布文章中直说:“我们重写了 React。”
2017 年 9 月发布的 React 16 已使用 Fiber,却没有把异步渲染能力默认交给所有应用。稳定版先带来 fragments、error boundaries、portals、自定义 DOM 属性与重写后的服务端渲染。新架构在 Facebook 和 Messenger.com 经过生产验证,未来调度能力仍谨慎隐藏。
这种发布节奏很有意思。重写内核通常诱惑团队顺便重做公共 API,React 反而努力让外部组件继续运行。官方通过兼容测试、警告和 codemod 把架构革命伪装成普通升级。React 16 的 react 加 react-dom 组合体积由官方统计的 161.7 KB 降至 109 KB,gzip 后从 49.8 KB 降至 34.8 KB,约下降三成;Fiber 更长远的收益则要到多年后才逐步开放。
为什么花这么久?因为可中断渲染会破坏许多隐含假设。组件若在 render 中做副作用,执行两次会出事;某些生命周期可能开始了却没有提交;库若以为一次更新必然连续完成,也会观察到陌生状态。调度器能暂停工作,生态代码未必允许自己被暂停。
Fiber 不是一个用户立刻感知的功能,它是 React 取得未来选择权的基础设施。代价是一段漫长过渡:Async Rendering、Concurrent Mode、Concurrent React,名称和 API 多次调整;一些生命周期被标记不安全;Strict Mode 在开发环境故意重复调用,以暴露非幂等代码。
React 开始管理的不只是界面长什么样,还有界面何时值得计算。
Fiber 把一次更新拆成可以调度的循环:
- 接收更新:把 state、props 或外部触发登记为待处理工作;
- 安排优先级:区分输入反馈、过渡更新与后台准备;
- 执行渲染:逐个 Fiber 计算下一棵组件树;
- 暂停或放弃:主线程需要让路时保存进度,结果过时则丢弃;
- 集中提交:只有完整结果准备好,才把变更提交给宿主环境。
RFC、发布通道与“还没稳定”的公开实验
Fiber 时代以后,React 的研究周期常常长于外界预期。某个概念先在大会演示,进入实验版本,API 改名,再等框架和库适配。对于追新闻的开发者,这像路线反复;对于需要保持兼容的基础设施,过早冻结错误接口会更糟。
2017 年 React 引入公开 RFC 流程,重大变更先写动机、设计和替代方案,让社区在代码合并前讨论。RFC 不是投票箱,核心维护者仍要做技术取舍,但设计过程留下可检索记录,外部库作者也能提前看见影响。
2019 年官方又系统解释发布通道。Latest 面向稳定版本;Next 更接近主干,供 React 自身测试;Experimental 用于可能永远不会稳定的研究能力。Facebook 内部会持续运行接近主干的构建,帮助团队在公开发布前发现回归。后来 Canary 让框架可以按固定版本采用已经较成熟、却尚未进入稳定主版本的功能。
这套机制有明显张力。框架需要提前集成,才能让 Server Components 之类的跨层功能成熟;普通用户看到框架依赖 Canary,又会担心生产系统踩在实验版上。锁定精确版本能控制变化,生态仍可能出现“React 稳定版没有,某框架早已在用”的认知差。
React 没法消除不确定性,只能给它贴标签、分通道、留讨论记录。基础设施创新无法等所有问题都想清楚再接触用户,也不能把研究原型伪装成稳定承诺。发布制度本身,成了技术架构的一部分。
错误边界与 Portal:组件树不等于 DOM 树
Fiber 的宏大目标容易盖住 React 16 解决的日常问题。
早期 React 中,渲染阶段发生异常可能让内部状态损坏,错误信息难懂,用户只能刷新页面。React 16 默认在未捕获错误时卸载整棵树,宁可显示空白,也不保留可能错误的界面;开发者可以用 error boundary 捕获子树异常,显示备用 UI。
这是一种产品态度:错误不能只在控制台里算“被处理”。支付金额、聊天对象或权限状态如果已经不可信,继续展示旧界面可能比白屏更危险。Error boundary 把故障隔离变成组件层面的责任,应用可以让某个侧栏失败,而不拖垮整页。
Portal 则允许子组件把 DOM 渲染到父组件 DOM 层级之外。模态框、提示层和浮动菜单常需要逃离 overflow、层叠上下文或布局限制。过去开发者要在组件归属与实际节点位置之间做别扭选择,Portal 明确区分两棵树:组件仍属于原来的 React 父级,事件和 Context 沿组件关系传播;DOM 节点可以挂到别处。
这两个功能都在强化同一件事:React 的抽象对象是组件关系,不是 DOM 文件夹。DOM 是一个 renderer 的提交结果,不能反过来完全决定应用结构。
Hooks:类组件的问题不在 class 这个词
2018 年 React Conf,Sophie Alpert 与 Dan Abramov 公布 Hooks。2019 年 2 月,React 16.8 将它稳定发布。此后几年,函数组件几乎成了 React 教程的默认语言。
如果只说“Hooks 让代码更短”,会错过它所要解决的核心问题。
类组件把相关逻辑拆在多个生命周期里。订阅在 componentDidMount 建立,在 componentWillUnmount 清理,props 改变后还要在 componentDidUpdate 重新同步。一个生命周期又可能混着标题更新、网络订阅和统计上报几件互不相干的事。复用有状态逻辑时,团队依赖 render props 或高阶组件,组件树套出层层包装,DevTools 里像一串俄罗斯套娃。
Hooks 允许函数组件调用 useState、useEffect 等能力,自定义 Hook 可以把一组状态与副作用协议抽成普通函数。它复用的是逻辑,不是渲染层级。与其说函数组件取代 class,不如说 Hook 为组件增加了另一种组合轴。
官方采取了很克制的迁移口径:不建议一夜重写旧应用,Hooks 与 class 可以并存,没有移除 class 的计划。React Native 在 0.59 支持 Hooks。这样的兼容承诺降低了采用风险,也让新旧范式长期共处。
Hooks 同时把一套新的执行模型暴露给开发者。Hook 必须按固定顺序调用,不能随意放进条件分支;闭包会捕获某次 render 的 props 和 state;Effect 依赖数组写少了会读旧值,写多了可能反复建立连接;对象与函数的引用变化又会触发子组件或 Effect。
这些问题不是语法瑕疵,它们来自 React 的函数式渲染模型。每次 render 都像一次新的函数调用,每个闭包属于那次调用。开发者如果把组件理解成“有一份变量会不断更新的对象”,就会和 stale closure 撞个满怀。
React 16.8 强烈推荐 eslint-plugin-react-hooks。也就是说,API 表面变轻,部分正确性却交给静态工具维护。只靠运行时,React 很难知道某个条件调用是不是故意,也无法判断漏掉的依赖。Hook 能够成为主流,靠的是语言约定、lint 规则、DevTools 和团队习惯共同组成的制度,函数写法本身并不足以取代类。
从 mixin 到 Hook,React 花了六年寻找逻辑复用
React 刚开源时,createClass 支持 mixin。组件可以混入订阅、窗口尺寸或权限逻辑,快速获得 state 与生命周期方法。这照顾了面向对象时代的使用习惯,也让早期采用容易一些。
规模上来后,mixin 的隐式依赖开始伤人。组件调用一个本文件没有的方法,要逐个翻 mixin 才知道来自哪里;两个 mixin 都定义 handleChange 会撞名;mixin 还能依赖另一个 mixin,共享同一 state 与方法命名空间。官方 2016 年《Mixins Considered Harmful》提到,Facebook 已出现大家不敢碰的组件,复杂性的一部分正来自这些扁平混入关系。
社区随后使用高阶组件:函数接收一个组件,返回包着它的新组件。订阅与权限逻辑有了明确 props 接口,却容易形成多层 wrapper,静态属性、ref 和命名调试也要额外处理。Render props 把复用逻辑放进带函数子节点的组件,数据流更显式,嵌套一多仍会挤占 JSX 结构。
Hooks 没有让此前模式“失效”。它改变了复用单位:自定义 Hook 只是函数调用,不增加组件层级,可以组合其他 Hook,返回值由作者明确命名。依赖仍可能复杂,至少不再把方法悄悄倒进同一实例。
这条演化路线说明 React 的“组合优于继承”也不是发布第一天就完成的答案。框架先提供迁移旧习惯的逃生门,真实项目暴露问题,再寻找更符合自身执行模型的抽象。今天看起来理所当然的 useSomething(),背后是 mixin、HOC 和 render props 轮流交过的学费。
它还解释了 React 为什么对 class 保持兼容。旧组件不是错误代码的博物馆,其中装着多年业务。新抽象若只能通过全量改写证明优雅,组织往往宁可不要这份优雅。Hooks 能逐个组件、逐段逻辑进入,才有机会成为主流。
测试文化也从“检查内部”转向“观察行为”
组件抽象出现后,测试该看什么成为新争论。React 早期提供 TestUtils 和 shallow renderer,Enzyme 又让开发者方便地找到组件实例、读取 state、调用生命周期。测试能精确确认内部结构,却容易和实现绑死:把 class 改成函数,用户看到的界面没变,大量测试先碎。
React Testing Library 推动另一种方向:尽量像用户一样通过文字、角色和可访问名称寻找元素,点击后观察页面结果,不把组件内部 state 当公共合同。这不保证测试自动优质,却与 React 的声明式承诺更一致——组件如何算出界面是实现,用户能否完成任务才是行为。
Hooks 与并发渲染又使测试时机复杂化。state 更新、Effect 与 Suspense 可能分几个阶段完成。React 16.8 加入 act(),要求测试把会触发更新的操作包起来,再在 React 刷新相关工作后断言。测试工具通常替用户调用它,泄漏时那句 “not wrapped in act” 仍会把许多开发者折腾半天(这条警告的教育方式,确实谈不上温柔)。
React 18 后,测试库还要适应可中断渲染、Strict Mode 重复调用和异步边界。核心团队改变调度器,测试生态必须重新定义“完成”是什么意思。一次点击后立刻读 DOM 的旧习惯,不一定代表用户最终看到的结果。
这段演变与 React 本身很像:从直接检查对象和节点,转向描述输入与可见结果;内部实现因此更自由,等待与查询的规则却更严格。抽象不会减少所有测试复杂性,它只是告诉团队,哪一部分复杂性值得写进长期合同。
Effect 的十年后遗症
Hooks 最有争议的部分一直是 useEffect。
它原本用于让组件与 React 之外的系统同步:网络连接、浏览器 API、订阅、第三方控件。可“副作用”这个词太宽,教程很快把请求数据、派生状态、事件响应、初始化和任何看起来不属于 JSX 的逻辑都塞进去。于是一个组件先渲染空状态,Effect 再请求,回包后再渲染;子组件此时才开始请求,网络瀑布就形成了。
依赖数组又让许多人产生一种错觉,好像它是“什么时候运行”的手动开关。实际它应当描述 Effect 使用了哪些响应式值。为了阻止重复执行而删除依赖,通常只是把同步关系藏起来。lint 报警被关掉,代码短期安静,几个月后读到旧 props。
React 新文档后来花了大量篇幅教人“你可能不需要 Effect”。能在 render 中算出的值直接算;用户点击造成的逻辑放进事件处理器;数据请求最好交给框架或专门缓存库;外部系统同步才是 Effect 的主场。文档不是突然转向保守,生态只是用几年真实项目交了学费。
React 19.2 的 useEffectEvent 又尝试拆开 Effect 中“需要响应依赖变化的同步逻辑”和“总要看到最新值、却不该让连接重建的事件逻辑”。例如聊天房间由 roomId 决定是否重连,连接成功提示需要最新主题颜色;主题变化不应让房间断开再连接。新 API 让这种事件读取最新值,又不成为 Effect 依赖。
一个 API 发展到需要专门文档、lint 和新 Hook 去解释边界,不代表它一无是处。它说明 React 把“同步外部世界”抽象成通用原语后,使用范围比设计意图扩张得更快。
老实说,Hooks 是 React 最漂亮也最别扭的一次成功。它让逻辑组合变得自然,又让闭包时间、引用身份和依赖关系成为每个初学者必须补修的课程。
React 17:一版“没有新功能”的基础设施
2020 年 10 月,React 17 发布。官方标题下最醒目的一句话是:没有新的面向开发者功能。
放在追求发布声量的软件行业,这近乎反营销。主版本通常要端出足够多的新 API,证明升级值得。React 17 却把主要精力用在让未来升级没那么危险。
此前,一个页面若同时运行两个 React 大版本,事件系统很容易互相踩踏。React 16 及更早版本通常把事件处理委托到 document,整页共享一个顶层入口。React 17 改为把事件绑定在各自根容器。这样,页面中的一块旧区域可以留在 React 17,另一块逐步升级到未来版本,非 React 应用嵌入组件也更稳。
官方把它称作 stepping stone release,一块过河用的踏脚石。大多数项目仍适合一次升级,加载两份 React 也不理想;可对于多年积累、缺少维护者的巨大代码库,“全部一起改”往往等于永远不改。渐进升级不是技术炫技,它承认组织改造有速度上限。
Facebook 用自身规模验证改动:官方称十万多个组件里,为适配 React 17 只修改了不到二十个。这个数字不是对所有公司的保证,却说明 React 团队衡量升级成本的单位早已不是示例项目。
React 17 还支持新的 JSX transform,并把支持回移到 16.14、15.7 和 0.14.10。新转换不再要求每个 JSX 文件显式 import React,但经典转换继续工作。连这种小语法便利也选择可选与回移,背后还是同一种治理习惯:React 可以变,用户不必同一天变。
没有新功能的版本,处理的是软件成功以后最不性感的问题——旧代码怎样不被未来抛下。
当组件越过浏览器,React 也开始失去边界
2022 年 3 月,React 18 把 Fiber 多年前准备的调度能力正式推到主线。自动批处理、startTransition、useDeferredValue、支持 Suspense 的流式服务端渲染一起出现,核心概念叫 Concurrent React。
“并发”很容易让人想到多线程。React 18 没有把普通 JavaScript 运算自动拆到几个 CPU 核上,也不会让一个死循环变流畅。它改变的是渲染工作的可中断性。
同步渲染一旦开始,会连续计算到可以提交。并发渲染可以启动一次更新,做一部分,暂停,让浏览器先处理更急的输入,再继续;如果期间来了更新结果,旧工作甚至可以被放弃。React 仍会等完整树准备好后再修改可见 DOM,避免用户看到半新半旧的界面。
这让更新出现优先级。用户在搜索框输入字符,输入内容必须立即显示;根据关键词过滤上万条列表,可以被标记成 transition,晚一点完成。列表计算的成本并没有消失,React 只是允许低紧迫工作让开主线程,先完成高紧迫反馈。
自动批处理处理了另一个日常细节。React 18 以前,React 事件处理器里的多次 state 更新通常合并一次渲染,但 Promise、setTimeout 或原生事件里的更新未必如此。新版本把更多场景自动批处理,减少无意义的中间 render。需要立即同步提交时仍有 flushSync,只是那是明确的逃生门。
并发渲染也要求组件更纯。一次 render 可能暂停、重复或丢弃,render 里的副作用就可能执行多次却没有对应提交。Strict Mode 在开发环境故意重复调用某些逻辑,很多开发者觉得它“制造 bug”;实际上它把生产中未来可能出现的不安全假设提前放大。
React 18 的发布策略仍然渐进。只有使用 createRoot 和并发能力的部分才启用新行为,旧应用可以先完成基础升级。官方还组建 React 18 Working Group,让框架与库维护者提前反馈。因为调度器一旦改变时间顺序,受影响的不是 React 自己,整个组件、测试和状态生态都要学会被打断。
Suspense:加载状态也要进入组件树
Suspense 的外观看起来很简单:子树还没准备好,就显示最近边界的 fallback。可它背后的野心远大于一个 loading spinner。
传统应用在每个组件中手写 isLoading、error 和数据状态。父子请求互相嵌套后,页面先出骨架,某块回来,下一块才开始,布局不断跳。加载顺序散在事件与 Effect 中,React 只看见每次请求结束后的结果,不知道整页何时算准备好。
Suspense 把“这部分暂时无法完成”变成渲染协议。组件可以暂停,边界决定用户先看到什么,流式 SSR 则允许服务器先发送页面外壳,再把准备好的片段插入。加载不再只是数据层的一枚布尔值,它进入界面结构。
这也解释了为什么 React 18 不建议普通应用随手用 Suspense 做临时数据请求。要避免瀑布,路由、缓存、请求生命周期和服务器渲染必须配合。官方列出 Relay、Next.js、Hydrogen、Remix 等有明确架构的环境。React 提供协调原语,框架负责把数据系统接进去。
从这里开始,“React 只是视图库”的说法越来越难维持。核心包当然没有自带数据库与路由,Suspense 的最佳效果却要求它知道代码、数据与渲染何时就绪。UI 边界开始反过来约束应用架构。
从 renderToString 到选择性 hydration
React 很早就能在服务器生成 HTML。Instagram 的外部化过程推动了 renderToString,2013 年官方也把 SEO、性能和代码共享列为服务端用途。可早期 SSR 更像一场视觉魔术:服务器先给用户可读 HTML,交互能力仍要等浏览器下载 JavaScript,再用同一组件树进行 hydration。
旧流程有三道“整页闸门”。服务器往往要等全页数据齐全才开始输出;浏览器要等全页组件代码下载完才开始 hydration;hydration 一旦开始,又会连续处理整棵树。评论区请求慢,导航和文章也跟着等。用户虽然早早看到按钮,点击时可能毫无反应,因为干燥的 HTML 还没接上事件。
React 16 重写服务器 renderer 并支持流式输出,主要改善发送速度。React 18 把 Suspense 接进服务器流程后,才允许不同边界分别通过这些阶段。服务器可以先发送页面外壳和 fallback,较慢内容准备好后继续流出;浏览器可以优先 hydration 用户正在交互的区域,不必让一个重量级评论组件堵住导航。
这叫选择性 hydration。它没有让下载和执行成本消失,而是拆掉“最慢部分决定整页进度”的顺序。性能从总耗时问题变成调度问题:哪块先可见,哪块先可点,哪些代码可以晚来。
SSR 也因此更依赖框架。服务器要收集数据、处理流、插入脚本、管理缓存与错误,客户端要匹配同一模块图。只调用一个 React API 能做实验,稳定生产还涉及路由与部署环境。React 在核心中提供流水线零件,框架开始负责整条装配线。
Server Components:组件越过了网络
2020 年 12 月,React 团队公开 “Zero-Bundle-Size React Server Components” 研究演示。当时官方反复强调仍在研发,不催社区立即采用。这个谨慎有理由:Server Components 不是给现有 SSR 换个名字,它重画了组件在哪儿执行的边界。
传统 SSR 在服务器把 React 树生成 HTML,浏览器下载 JavaScript 后 hydration,让同一界面恢复交互。组件代码通常仍会进客户端 bundle。Server Component 则只在服务器或构建环境执行,可以直接读取数据库、文件和服务端依赖,它的代码不发送到浏览器。需要交互的 Client Component 保留在客户端,两种组件通过协议组成一棵树。
这解决了几个长期矛盾。Markdown 解析器、日期库或数据访问代码可以留在服务器,减少客户端 JavaScript;数据读取可以靠近组件,不必把每个字段再做成一层 HTTP API;服务器返回的不是最终 HTML 字符串,而是一种能与客户端组件状态合并的表示。
听起来像免费午餐,边界规则却非常具体。Server Component 不能使用浏览器状态与 Effect,Client Component 不能直接访问数据库;从服务器传给客户端的 props 必须可序列化;"use client" 会划出模块图边界,边界下的依赖可能进入客户端 bundle。设计不好,一句指令就能把本想留在服务器的代码带回浏览器。
RSC 也不同于 SSR。一个应用可以有 Server Components 却在请求时或构建时产生表示,也可以结合 SSR 把首屏输出为 HTML。两者解决不同层级:RSC 决定组件代码与数据在哪执行,SSR 决定初始页面怎样变成可见 HTML。
React 19 把面向使用者的 Server Components 能力纳入稳定范围,却提醒框架和打包器作者:底层实现 API 在 19.x 小版本间仍可能变化,最好锁定具体版本或使用 Canary。这句注释很重要。对普通开发者,“稳定”意味着写组件的模型可用;对实现框架的人,脚下仍是施工地面。
缓存成为树里看不见的第二份状态
组件在服务器读取数据后,一个问题立即追上来:同一请求要不要复用?下一次导航呢?不同用户之间呢?结果保存多久,什么操作让它失效?
客户端 React 的 state 通常跟组件实例生命周期绑定,开发者能在树上找到它。服务器缓存可能存在进程、请求、数据中心或 CDN,生命周期不再直观。框架为了性能复用结果,用户却可能看到过期数据;每次都重新获取更容易理解,又会失去 RSC 与预渲染的许多收益。
React 提供 cache 等原语,19.2 的 cacheSignal 还能在某次缓存生命周期结束时中止或清理工作,但缓存策略仍主要由框架与部署环境决定。Next.js 某个版本的默认缓存行为,不等于 React Server Components 本身的永恒语义。把两者混写,是许多教程让人迷惑的来源。
Server Actions 又必须在变更数据后触发正确的失效与刷新。乐观界面可以先变,服务器事实稍后确认,缓存再更新。这里同时存在用户看到的 UI state、服务器持久数据、请求中的临时结果和框架缓存。所谓“全栈 React”并没有把状态合成一种,反而要求工具明确协调四种时间。
这也是 RSC 学习成本的核心。语法只有 "use client" 和 "use server" 几个词,难点在于数据跨过边界后由谁保存、何时重算、失败怎样回滚。组件靠近数据能减少胶水代码,也把缓存治理带进组件架构。
Next.js 与 Vercel:合作越成功,边界越敏感
Server Components 没法只靠 react 包落地。它需要路由决定服务器与客户端导航,需要打包器理解模块边界,需要部署平台执行服务器函数与流式响应,还要处理缓存、预取和错误恢复。Next.js 成为最早、最完整的承载者。
这段合作推动了 RSC 从研究演示进入真实项目。Next.js App Router 把 Server Components、嵌套路由、布局、Server Actions 和流式渲染组合成开发体验。React 团队获得大规模反馈,Vercel 则获得与 React 未来方向深度绑定的产品差异。
成功也带来怀疑。
React 文档越来越推荐框架,新能力先在 Next.js 中可用,社区会担心核心路线是否被一家托管公司左右。其他打包器若必须追逐未稳定的 RSC 协议,就很难提供同等体验。对使用者来说,“这是 React 的限制,还是 Next.js 的选择”也常混在一起。
简单指控 Vercel “绑架 React”不准确。React 18 已公开与 Next.js、Remix、Hydrogen 等合作,2025 年 RSC 漏洞修复又涉及 React Router、Waku、Parcel、Vite RSC 插件和 RedwoodSDK。全栈功能天然需要深度集成,最早投入最多的框架也必然走在前面。
但结构性风险真实存在:当一个库的关键价值只能由少数框架兑现,框架实现者就获得解释标准的权力;当框架商业模式又与托管平台相连,技术默认值会影响部署选择。要约束这种风险,就得承认利益关系,稳定公共协议,扩大实现数量,并把技术治理与资源董事会分开。
React Foundation 后来的治理设计,部分就在回应这种成熟生态的敏感性。
React 19:表单提交为什么成了核心能力
2024 年 12 月,React 19 稳定发布。最醒目的新概念是 Actions。
一次表单提交看似普通,真实应用却要管理 pending、错误、乐观更新、并发请求、成功跳转和表单重置。过去这些状态由开发者分别放进 useState,请求开始设为 true,结束设回 false,失败回滚,成功刷新。每个项目都会写一遍,边角条件也各漏一遍。
React 19 允许异步函数在 transition 中作为 Action,自动关联 pending 与错误处理;useActionState 管理 Action 的结果和状态,useOptimistic 先展示用户期望的结果,useFormStatus 让按钮读取父表单状态,<form action={fn}> 把函数直接接到提交过程。React 把一套反复出现的异步 UI 协议收进核心。
新的 use API 可以在 render 中读取 Promise 或 Context。读取尚未完成的 Promise 时,组件暂停并交给 Suspense 边界。它和普通 Hook 不完全一样,可以在条件分支中调用,却仍只能处于 render。官方同时提醒,不要在 render 中临时创建未缓存 Promise,应由支持 Suspense 的库或框架提供。
这些 API 让客户端表单更省事,与 Server Actions 结合时又能让客户端组件调用服务器异步函数。框架根据 "use server" 建立引用,客户端发出请求,服务器执行后返回结果。这里必须纠正一个常见混淆:"use server" 标记的是可远程调用的服务器函数,不是每个 Server Component 的标志;Server Component 本身没有对应指令。
React 19 还允许函数组件把 ref 当普通 prop,改进 hydration 错误差异,提供等待数据完成的静态预渲染 API。这些改动放在一起,React 的关注点已经从“点击后改哪个 DOM”走到“异步操作跨客户端与服务器完成时,界面如何保持连贯”。
框架边界又向外挪了一格。
19.2:渲染器开始管理“暂时看不见的未来”
2025 年 10 月发布的 React 19.2 没有 React 19 那样醒目的范式口号,却把并发和服务器方向继续推深。
<Activity> 允许把应用区域设为 visible 或 hidden。隐藏时,DOM 可以保留,Effect 被卸载,更新降到较低优先级;重新显示时,输入值等状态能够恢复。它适合预渲染用户可能访问的下一页,也适合返回时保留旧页面状态。
过去条件渲染通常意味着组件不存在,状态也随卸载丢失。Activity 把“不可见”从一个布尔判断升级成调度状态:内容虽然暂时不在用户眼前,React 仍能在空闲时准备它。Fiber 当年争取的时间控制权,到这里变成普通组件 API。
19.2 还加入部分预渲染。应用的静态外壳可以提前生成并放进 CDN,动态部分在请求到来后从保存的 postponed state 继续渲染。它试图把静态生成与动态 SSR 从二选一变成同一棵树的两个阶段。
这些能力通常不会由业务组件直接裸用。框架要决定路由何时隐藏、资源如何预取、预渲染结果怎样缓存。React 核心提供更细的渲染状态,框架把它们编排成产品路径。两者的界线越来越像操作系统内核与发行版:理论上可以只用内核,普通人使用的却是整套组合。
React Compiler:把纪律从运行时搬到构建期
React 长期要求开发者理解引用身份。父组件每次 render 都创建新对象或函数,子组件的 memo 可能失效;昂贵计算用 useMemo,回调用 useCallback。写得多了,组件里到处是为了优化而存在的依赖数组,核心业务逻辑反而被包在中间。
编译器是 React 对这笔手工税的回答。
团队探索编译优化并非突然起意。2017 年的 Prepack 尝试提前执行 JavaScript,项目后来停止,却影响了 Hooks 设计。2021 年 Xuan Huang 演示新编译器原型,后续团队多次重写架构,采用基于控制流图的高级中间表示,分析数据流、可变性与 React 规则。
2025 年 10 月,React Compiler 1.0 稳定。它作为构建期工具自动 memoize 组件和 Hook,不要求重写现有代码,并兼容 React 17 及以上版本。由于能理解条件分支与具体值使用,它有时可以做到手工 useMemo、useCallback 难以表达的细粒度缓存。
编译器不只是性能插件。它的验证阶段编码了 Rules of React,通过 eslint-plugin-react-hooks 暴露违反纯度、在 render 中 setState、不安全读取 ref 等问题。原来靠文档与 lint 猜测的纪律,开始由更强的数据流分析支撑。
可别把它写成“从此不用思考性能”。官方明确保留 useMemo 和 useCallback 作为精细控制的 escape hatch,尤其当稳定引用还是 Effect 依赖协议的一部分。既有项目也不应为了代码好看一口气删除手工 memoization,因为删除可能改变编译输出,进而影响依赖某些引用稳定性的 Effect。
Meta 公布过 Quest Store 的早期结果:初始加载和跨页导航最高改善约 12%,部分交互快于原来的 2.5 倍,内存大致不变。官方也紧跟一句“实际效果因项目而异”。编译器不是把每个应用统一加速,它更像自动寻找开发者原本可能写、也可能忘记写的缓存边界。
React 最初用运行时 reconciliation 允许开发者“重新描述整个界面”;十二年后,它又用编译器降低这种写法的运行时成本。路线没有放弃运行时,而是多加一层理解 JavaScript 的机器。
2025 年安全事故:前端库第一次像服务器框架那样补洞
2025 年 11 月 29 日,Lachlan Davidson 向 Meta 漏洞赏金计划报告 React Server Components 的安全问题。12 月 3 日,React 团队公开 CVE-2025-55182:攻击者可以构造恶意请求,利用 React 解码 Server Function payload 的缺陷,在未认证情况下实现服务器远程代码执行。CVSS 评分 10.0。
公告要求受影响用户立即升级 react-server-dom-webpack、react-server-dom-parcel 或 react-server-dom-turbopack 的修复版本。Next.js、React Router 的实验性 RSC、Waku、Parcel RSC、Vite RSC 插件和 RedwoodSDK 等集成也进入升级清单。托管商部署了临时缓解,React 团队仍强调不能依赖缓解替代升级。
影响范围必须说准。纯客户端应用若不使用服务器,不受这个漏洞影响;没有采用支持 RSC 的框架、打包器或插件,也不在同一范围。把新闻标题缩成“所有 React 网站都能被接管”既不专业,也会让受影响的团队错过判断条件。
随后官方又披露拒绝服务与源代码暴露问题。源代码暴露主要涉及硬编码在 Server Function 源码里的秘密,环境变量一类运行时秘密不在同一暴露方式内。到 2026 年 1 月,补丁时间线仍在更新。
这组事故值得写进历史,关键不在漏洞数量,在责任变化。
2013 年的 React 处理浏览器中的轻量界面描述。2025 年的 React 在服务器反序列化来自网络的 payload,把引用翻译成函数调用,并协调框架、打包器与托管平台发补丁。攻击面不是偶然闯入,它跟着能力一起进入。
React 可以说底层协议由框架使用,框架也可以说协议来自 React。用户不会关心责任怎样分摊,他们只想知道该升级哪个包。这迫使生态建立更成熟的安全发布、版本锁定和跨项目通报机制。RSC 把 JavaScript 从浏览器 bundle 中拿走一部分,也把服务器安全这份新账单递了进来。
技术扩张从来不是只拿能力,不拿责任。
React Foundation:Meta 放下所有权,不等于退出影响力
2025 年 10 月,React 团队宣布建立 React Foundation 的计划。2026 年 2 月 24 日,基金会在 Linux Foundation 托管下正式成立。
React、React Native 和 JSX 等支持项目不再归 Meta 所有。八家白金创始成员是 Amazon、Callstack、Expo、Huawei、Meta、Microsoft、Software Mansion 和 Vercel。基金会董事会由成员代表组成,Seth Webster 任执行董事,负责基础设施、商标、会议、资金与生态项目。
官方特意把技术治理与基金会董事会分开。技术方向继续由日常贡献和维护 React 的人决定,临时领导委员会负责设计更具体的结构。到正式成立时,仓库、网站与基础设施迁移仍在进行,技术治理细则也没有全部定稿。
这种分离不是形式主义。董事会成员投入资金,当然希望生态健康;它们也各有云服务、移动工具、咨询或框架利益。若出资额直接换成 API 决策权,基金会只会把“Meta 控制”改成“联盟控制”。把资源治理和技术治理拆开,至少在制度上承认两种权力不该自动等价。
Meta 也不会因为移交所有权就失去影响力。它仍有长期维护者、巨型生产代码库和持续投入,工程经验会自然影响路线。Vercel、Microsoft、Expo 等同样通过框架、工具与雇员参与获得影响。基金会的职责是让这些影响通过更可见的规则发生,避免任何一家公司的产品变化直接变成项目所有权变化;它无法把影响本身清零。
基金会不是“社区接管”的完成时
把项目迁入 Linux Foundation 很容易被写成独立大结局,正式公告其实用了大量将来时:仓库、网站和基础设施还要转移,技术治理结构还要定稿,生态支持计划也待建立。法人归属改变是重要起点,不会自动解决维护者负担、路线透明和框架代表性。
“厂商中立”能否落地,要看日常权力怎样运行。谁能成为维护者,重大 RFC 如何收敛,安全事件由谁协调,不同 renderer 与框架是否有稳定发言渠道,会议和资助是否只照顾最有商业资源的成员?这些细节比董事会名单更有判断力。
八家创始成员的构成已经反映 React 的责任范围:云平台需要它稳定部署,移动公司依赖 React Native,框架与咨询团队维护生态,Meta 仍贡献核心经验。Huawei 在正式成立时加入,也让成员范围不只局限于最初七家公司。多元利益可以互相制衡,也可能让决策变慢;慢一点有时正是基础设施需要支付的保险费。
社区同样不能只要求企业交出控制,再假设志愿劳动会填满空缺。CI、发布、安全响应、文档与大会都需要长期资源。基金会的价值要在几年后衡量:它是否把公司投入转成不依赖某家公司善意的公共能力。
从 2017 年许可证危机到 2026 年基金会,React 用九年才把“谁拥有代码、专利、商标和技术方向”逐层拆开。一个内部工具开源时,Meta 的集中投入是优势;当它成为多家公司共同基础设施,同一种集中又会变成信任风险。
组织结构也必须 reconciliation。
React 到底留下了什么
如果把 React 的遗产压成一张 API 清单,很多项目已经过时。createClass 退出主流,mixins 被官方劝退,类生命周期让位于 Hooks,CRA 从推荐入口变成维护模式,Concurrent Mode 这个名字也换成了 Concurrent React。今天最流行的写法,几年后仍可能被新文档重新解释。
更稳定的遗产,是几种已经进入行业日常的判断:
- 界面可以被当作状态的函数来描述;
- 组件边界可以同时服务代码结构与团队协作;
- 宿主环境不必决定组件模型,renderer 可以更换;
- 异步会影响界面何时完整、哪部分先显示、哪些工作值得继续;
- 兼容性不是保守的同义词,而是创新抵达真实代码库的运输系统。
React 也留下了一串没有彻底解决的问题。状态应该局部还是全局?运行时协调与编译期更新怎样取舍?组件该知道多少数据来源?服务器与客户端边界由谁决定?核心库、框架、托管平台和基金会之间的权力怎样透明?每一代 React 都给出新答案,又让下一代问题更大。
我不太赞成把 React 写成前端的唯一正确道路。Vue 证明模板与组件可以有另一种平衡,Svelte 把更多工作移到编译期,Signals 路线重新强调细粒度响应,服务端模板和 Web Components 也从未消失。React 的流行有技术质量,也有 Facebook 的规模、人才投入、招聘市场、社区网络和框架商业化共同作用。胜利从来不只发生在 benchmark 里。
可它确实改变了行业讨论方式。今天即使一个框架公开反对 Virtual DOM,也往往仍在谈组件、声明式界面、状态所有权、服务端与客户端边界——这些问题有很多不是 React 发明的,却被 React 推进了普通开发者的日常工作。
再回看 2013 年那段古怪的 JSX,令人惊讶的已经不是大家接受了“在 JavaScript 里写标签”,而是另一份合同成了行业常识:开发者描述现在,系统负责把过去变成现在。
十三年里,那个“系统”从几千行浏览器代码长成了调度器、移动 renderer、服务器协议、编译器和基金会。每多接一层,它就更有能力,也更难继续自称只是一件轻巧工具。
屏幕上,一个组件仍只是返回几行 JSX。
屏幕背后,已经站着整段前端工业史。
资料来源
- React 官方:Our First 50,000 Stars
- React 官方:Why did we build React?
- React 官方:One Year of Open-Source React
- React 官方:Flux: An Application Architecture for React
- Redux 官方:The History of Redux
- React 官方:React v0.14
- React 官方:React v15.5
- Meta Engineering:React Native: Bringing modern web techniques to mobile
- React Native 官方:React Native for Android
- React 官方:Create Apps with No Configuration
- React 官方:Sunsetting Create React App
- Apache Software Foundation:Third Party License Policy
- Matt Mullenweg:On React and WordPress
- Meta Engineering:Relicensing React, Jest, Flow, and Immutable.js
- React 官方:React v16.0
- React 官方:Mixins Considered Harmful
- React 官方:Introducing the React RFC Process
- React 官方:React v16.8: The One With Hooks
- React 官方:Preparing for the Future with React Prereleases
- React 官方:React v17.0
- React 官方:Introducing Zero-Bundle-Size React Server Components
- React 官方:React v18.0
- React 18 Working Group:Streaming HTML and Selective Hydration
- React 官方:React v19
- React 官方:React 19.2
- React 官方:React Compiler v1.0
- React 官方:Critical Security Vulnerability in React Server Components
- React 官方:Denial of Service and Source Code Exposure in React Server Components
- React 官方:The React Foundation