前端项目写久了,很多人都会遇到一个别扭的阶段:
组件明明已经拆得很细,页面代码却越来越乱。
按钮、弹窗、表单、表格,一个个看上去都很“纯”,但一到真正接业务的时候,页面层开始塞满 props、事件回调和各种 hook。组件像一群嗷嗷待哺的孩子,页面像一个被迫同时照顾所有人的家长。
问题通常不是“拆得不够细”,而是状态边界没有划清。
很多团队会强调组件要“纯”:
这套原则本身没错,而且对基础组件特别重要。
比如按钮应该只关心文案、样式和点击;弹窗应该只关心展示内容和开关;表单输入项应该只关心当前值与变更事件。
问题在于,如果把这套原则机械地推到所有层级,页面会开始承担过多责任。
于是你会看到这些症状:
这并不是“代码更清晰”,而是“复杂度被转移了位置”。
这层中间地带,很多人会叫它 container。
你可以把它理解成“业务容器组件”:它不像基础组件那样极致纯粹,但也不该膨胀成页面级大杂烩。
它的作用是把某一块相对内聚的业务状态和交互逻辑包起来,让页面层只负责拼装,让基础组件只负责展示。
这其实是在回答一个很现实的问题:
一段逻辑,到底该放进组件、页面,还是全局 store?
如果不引入 container 这一层,答案往往只剩两个极端:
而这两个极端都不好维护。
内聚型 container 适合那种“自己就能闭环”的功能块,比如一个申请表单、一个上传面板、一个搜索框组合。
它通常同时管理:
外部页面只需要把它当成一个相对完整的业务模块来使用。
这类 container 的好处是,状态和行为都围绕同一个目标组织,不会散落在页面各处。你甚至可以把它拆成 “UI 组件 + 自定义 hook”,既保留可测试性,又不至于让页面知道太多细节。
还有一类更常见的场景,是页面由很多“单点功能”组成,但这些功能之间其实存在明显关系。
例如一个复杂表格页:
如果所有按钮、弹窗开关、回调函数都平铺在页面组件里,代码很快就会炸开。
这时更合理的做法,往往是按职责重新组合:
TableContainer 负责表格及其行操作DialogGroup 负责所有相关弹窗这样做不是为了“看起来优雅”,而是为了让相关逻辑待在一起。你以后改弹窗,不必先在页面里翻半天;你以后加一个按钮,也不必顺手再多塞两个 state 到页面根部。
很多人一碰到复杂度,就想把状态直接扔进全局 store。
这招有时有效,但如果没有节制,副作用也很明显:本来只在某个局部功能里有意义的状态,被提升成了全局知识,最后谁都能读、谁都可能改,边界反而更模糊。
一个更稳妥的判断方式是:
这样一来,store 真正承载的是“共享的业务事实”,而不是所有懒得传 props 的东西。
如果项目不大,当然可以用 Context API 解决一部分状态共享问题,尤其是主题、语言、当前用户这类天然跨层级的信息。
但 Context 更像“传递机制”,不是完整的状态管理方案。
它最适合解决的是 props drilling,也就是属性逐层透传;一旦你把很多频繁变化的业务状态都塞进去,就会开始面对消费者重渲染、Provider 层层嵌套、数据流不易追踪这些问题。
所以 Context 能用,但不该默认承担所有共享状态职责。
把组件、container、store 分开,不是为了发明新名词,而是为了回答三个问题:
一旦这些边界清晰,代码会出现一个很明显的变化:不是代码量变少了,而是“每一层只承担自己该承担的复杂度”。
这比单纯追求“组件越纯越好”更接近真实工程。
好的组件系统,不是把所有组件都训练成只会接 props 的乖孩子,而是知道什么时候该纯,什么时候该内聚,什么时候该提升为共享状态。
如果页面已经开始变成“所有逻辑的垃圾回收站”,那往往不是你拆组件拆少了,而是缺了一层 container,或者把本该局部处理的状态过早塞进了全局。
状态边界一旦划清,组件才会真正变得好用,页面也才会重新变得轻松。