组件是答案,也是问题
长期以来,React社区将“组件化”奉为圭臬,仿佛只要把界面拆成一个个小盒子,就能得到清晰、可维护的架构。然而,当业务复杂度上升,这种盒式思维便开始反噬。组件内部的state、props、hooks层层嵌套,我们不得不用render props、children、高阶组件或一堆custom hooks去缓解“状态提升”带来的地狱。事实是,组件本质上是UI的切割单元,而不是逻辑的天然边界。把逻辑硬塞进组件树,就像用贴纸固定水管,看似整洁,实则脆弱。
我们看到太多所谓的“可复用组件”其实只是在复用外观,而不是复用行为。一旦涉及跨层级的数据协同或异步事件,组件树就会变成一团纠缠不清的依赖网。useEffect、useCallback、useMemo的滥用让代码充满了执行顺序的隐形契约。这不是开发者的错,而是工具范式在引导我们用错误的维度思考。真正的错误在于,我们默认了“渲染树”应该等价于“状态流图”,默认了所有依赖都必须从父级流向子级。这种单向数据流的光环下,隐藏着一个可怕的假设:所有逻辑所属权都必须有且只有一个父节点。
因此,我们必须承认:React的组件模型是建模UI的利器,但绝不适合作为建模业务逻辑的主要容器。我们需要一个更独立的逻辑层,以及一种能让逻辑层优雅地与UI层松耦合的机制。否则,任何规模增长都会伴随着不成比例的复杂度爆炸,最终让团队陷入“重构基本盘”的死循环。
对比度:Hooks是止痛药,而不是疫苗
Hooks的诞生确实是一场进步,它让逻辑复用不再必须通过高阶组件嵌套。但仔细审视,useState、useEffect只是把原本属于组件实例的内部状态,转换成一种更细粒度的“函数内闭包”。这种闭包依然挂载在组件生命周期上,依然受到渲染调度的影响。当你用Recoil、Zustand或Redux Toolkit时,其实是在React之外的独立状态下建立旁路,然后再通过订阅机制把状态映射回组件。这种“外部存储+订阅”的做法,远比在组件内部玩useReducer要符合架构要求。但我们却很少细想,为什么非要组件树来驱动状态订阅,而不是用一个更原生的上下文机制来承载这些映射?
真正的对比度在于:传统组件是“显式所有者”,父组件拥有数据并向下传递;而现代响应式架构是“隐式消费者”,任何组件都可以直接读取满足其需求的上下文,无需关心谁产生、谁更新。React Context API虽然提供了跨层传递,但它的设计粒度太粗——任何上下文值变化都会导致所有消费者重渲染,这使得它几乎无法用于高频更新场景。于是我们不得不引入selector库,或手动拆分context。这恰恰暴露了React原生能力的缺失:它没有一套完备的“响应式依赖追踪”机制。对比Vue或Solid,React的更新粒度依赖于“组件函数是否重新执行”,而不是“具体哪个数据依赖发生了变化”。这种粗糙的响应性,导致了无数次的shouldComponentUpdate、memo,或手工的useSyncExternalStore。
因此,我的“全新独立观点”是:抛弃将“Hooks视为逻辑复用终极方案”的幻觉。Hooks只是把组件内部逻辑局部化,并没有打破跨领域、跨页面的逻辑共享困境。我们需要构建一层完全独立于组件树的“业务上下文层”,使用真正的响应式系统来管理所有状态和副作用。然后通过一个轻薄的适配器,让React组件仅仅成为一个“视图模板”,订阅上下文层中的切片。这样,组件树就不再是逻辑的所在之地,它回归了作为视图分层的本质。
上下文革命:为逻辑与UI建立断面
什么是“上下文革命”? 我认为,我们应该把Context从Provider/Consumer的实用工具,上升为一种架构哲学。 一个合理的React应用,应该由三层构成:视图层(纯粹展示组件)、适配器层(连接视图与逻辑的hooks),以及业务上下文层(独立于React的reactive store)。 关键的转变是,让业务上下文层拥有完全的自治权:它定义状态、动作、异步流程以及派生数据,且不依赖React的任何结构。 组件只需要用useContextSelector(或等效的useEffect+外部store)来绑定到上下文层中的某个切片。 这样,业务逻辑可以被单独测试、被多个应用复用,甚至可以部署到非React环境中(如service worker)。
这个模式带来的最直观改变是,我们不再需要在组件树里层层提升状态。 订单详情页不再因为要同时修改购物车和库存而将其state提升到App根节点。 每个逻辑域,如session、cart、notifications,都是独立的响应式模块。 组件通过一个简单的selector读取所需数据,并通过action触发逻辑变化。 React只是这些模块的忠实投影仪。 更重要的是,这种结构让渲染范围完全受控:只有当与当前组件绑定的数据片发生变化时,该组件才会重渲染。 这比React自带Context,以及多数全局状态库都要精确得多,且不需要任何手工优化。
当然,这种“上下文革命”并不是要杀死函数式组件或Hooks。 恰恰相反,我们的Hooks变得更加聚焦——它们只负责“连接”而不负责“实现”。 一个典型的连接器可能长这样:\ntsx\nfunction useOrderContext() {\n return useSelector(store, s => s.orders.filter(o => o.userId === s.currentUserId));\n}\n\n 这里的useSelector是我们自己封装的一个与React调度兼容的订阅机制,但内部逻辑完全由外部store驱动。 这种模式在技术上完全可行,并且已在Recoil和Jotai的简化版中得到了证明。 可惜的是,目前社区的主流心态仍然是“让React自己管理所有状态”,而不是“让React只负责视图”。 我断言,未来十年的前端架构趋势,必然是逻辑与视图的深度解耦。 谁能率先建立起这种上下文断面,谁就能在复杂度面前保持可持续的演进能力。
落地:从消灭useState开始
如果你认同上述观点,或许你已经意识到,当前项目中的每一个useState都是一个需要被迁徙到上下文层中的“候选成员”。 这并不是说任何时候都不能用useState——一个只在组件内部且不跨逻辑域的临时UI展开开关,用useState完全合理。 但任何与业务语义(登录用户、订单列表、主题偏好)相关的状态,都不应该躺在组件实例内部。 一个简单的方法论是:如果这个状态会影响其他不相关组件的行为,或者可能被后续业务扩展引用,那么它就应该被移入一个独立的上下文模块中。 这样做之后,组件之间的传参将减少到只传递纯UI配置(如className、style),而非数据。 我们得到的是更薄的组件文件、更少的手写useEffect同步,以及一种更接近数据流本质的编程体验。
另一个容易被人忽略的好处是,上下文层为跨页面的状态恢复、缓存和中止提供了天然的栖息地。 在React组件中,我们很难优雅地处理“页面切换后返回”时的状态保留。 但有了独立的上下文模块,你完全可以保留store中的映射数据,甚至实现持久化。 例如,当用户从商品详情页跳回列表页,滚动位置和过滤条件若存于上下文层,就能完美恢复——这比在组件里堆一堆sessionStorage操作要干净得多。 届时,React组件将变成“无状态”的视图函数,它们对给定的上下文切片渲染出一棵确定的界面树。 这种模式让人想起即将成为历史的MVC,但在现代响应式架构中,它获得了新的生命力。 最终,我们可以像搭积木一样组合逻辑模块与视图模块,而不再需要从顶层规划一棵所谓“组件树”的物理结构。
这就是我想批判并重塑的核心:让组件回归“视图”,让上下文承载“逻辑”。 这不是对React的攻击,恰恰是对React未来的救赎。 不要再问“什么状态的提升方式最好”,而要问“我如何能不看组件树就能理解业务逻辑”。 如果此文能激发你对React架构的重新思考,那我预期的“上下文革命”就真正开始了。