Unstated状态管理原理:轻量级React容器模式解析

📅 2026/6/23 0:29:13
Unstated状态管理原理:轻量级React容器模式解析
1. 项目概述为什么 Unstated 曾是 React 状态管理的“轻量级解药”你有没有在写一个中等复杂度的 React 项目时突然发现useState像个刚学会走路的孩子——够用但一碰到跨组件通信、逻辑复用、状态持久化就踉跄着要摔跤useReducer又像穿了双不合脚的皮鞋写起来板正费劲调试起来满屏action.type打转而 Redux哪怕只用最简配置光是store.js、actions/、reducers/这几个文件夹建出来心里就先打了个问号我真需要这么重的仪式感来管几个按钮开关和表单输入吗这正是 Unstated 在 2018–2020 年间被大量中小型团队悄悄采用的核心场景——它不试图替代 Redux 的企业级能力而是精准切中了一个被长期忽视的“中间地带”需要结构化、可复用、跨组件共享的状态但又拒绝为复杂度买单的务实型开发需求。Unstated 的核心设计哲学非常朴素把状态和操作逻辑封装进一个Container类再通过Provider和Subscribe组件后来演进为useContainerHook完成注入与消费。它没有自己的中间件生态不搞 action/reducer 拆分甚至不强制要求 immutable 更新——你直接this.setState({ count: this.state.count 1 })就完事。这种“类 React 自身思维”的一致性让开发者几乎零学习成本上手。我当年在做一个内部数据看板项目时三个独立图表组件需要共享同一组筛选条件时间范围、地域、指标维度用 Context API 手写 Provider 要处理嵌套、更新粒度粗、调试困难改用 Unstated 后一个FilterContainer类搞定全部逻辑Subscribe to{[FilterContainer]}一行代码注入状态变更自动触发最小范围重渲染。它不是银弹但对很多真实业务场景而言是恰到好处的那把小刀——不锋利到割伤自己也不钝拙到削不动苹果皮。2. 核心设计与思路拆解从 Context API 到 Container 模式的范式迁移2.1 为什么不是直接用 Context API—— 看得见的坑与看不见的债很多人第一反应是“React 不是有 Context API 了吗干嘛还要 Unstated” 这是个极好的问题答案藏在 Context 的底层机制里。Context 的本质是一个广播系统当Provider的value发生变化时所有订阅了该 Context 的组件都会收到通知。问题就出在这个“所有”上。假设你有一个UserContext里面包含user.name、user.avatarUrl、user.permissions三个字段。现在一个只显示头像的Avatar /组件订阅了这个 Context。当用户修改了权限列表permissions变更Avatar也会被强制重渲染——尽管它的render函数里压根没用到permissions字段。这就是著名的Context 更新粒度粗问题。官方文档里那句 “Context is primarily designed for sharing data that doesn’t often change”Context 主要用于共享那些不常变化的数据绝非客套话而是血泪教训。我曾在一个电商后台项目里把整个AppState含购物车、用户信息、全局提示、路由状态塞进一个 Context结果每次添加一个商品到购物车侧边栏菜单、顶部导航、甚至页脚版权信息都跟着闪一下。性能分析工具里Avatar、MenuLink这些组件的render时间飙升根源就是 Context 的“无差别广播”。Unstated 的解法非常聪明它不暴露原始 state 对象而是暴露一个带有setState方法的 Container 实例。消费者拿到的不是数据快照而是一个“活”的对象引用。这意味着Subscribe组件内部可以精确地监听container.state.count的变化而不是监听整个container.state对象。其底层实现依赖于setState调用时触发的forceUpdate并结合shouldComponentUpdate或useEffect的依赖数组做精细控制从而天然规避了 Context 的粒度问题。这不是魔法而是对 React 生命周期和更新机制的一次深度、务实的利用。2.2 Container 模式状态、逻辑与生命周期的三位一体封装Unstated 的灵魂在于Container类。它不是一个空洞的接口或抽象基类而是一个承载了完整状态管理生命周期的实体。我们来看一个典型的CounterContainerimport { Container } from unstated; class CounterContainer extends Container { state { count: 0, lastUpdated: null }; increment () { this.setState({ count: this.state.count 1, lastUpdated: new Date().toISOString() }); }; decrement () { this.setState({ count: Math.max(0, this.state.count - 1), lastUpdated: new Date().toISOString() }); }; reset () { this.setState({ count: 0, lastUpdated: new Date().toISOString() }); }; }这段代码揭示了 Container 模式的三大支柱状态Statestate是一个普通 JS 对象定义了容器的初始数据。它不像 Redux 那样要求 immutable你可以用任何你喜欢的方式更新它setState内部会做 shallow merge。逻辑Logic所有与状态相关的业务逻辑increment,decrement,reset都作为类方法定义在 Container 内部。这带来了两个关键好处一是逻辑与数据强绑定避免了useStateuseCallback组合中常见的闭包陷阱比如increment里用到了过期的count值二是逻辑天然可复用同一个CounterContainer实例可以在多个地方被Subscribe它们共享同一份状态和逻辑就像一个真正的“单例服务”。生命周期LifecycleContainer 类本身就是一个标准的 JavaScript 类你可以自由地在构造函数中初始化数据比如从 localStorage 读取、在方法中调用外部 API、甚至挂载setTimeout或addEventListener。它没有 React 组件的生命周期钩子但它拥有了比组件更纯粹、更可控的生命周期管理权。我曾用它封装一个WebSocketContainer在constructor里建立连接在disconnect方法里关闭连接并在onMessage回调中调用setState更新 UI。这种将副作用与状态更新紧密结合的能力是纯 Hook 方案难以优雅实现的。2.3 Provider/Subscribe 架构极简主义的依赖注入Unstated 的顶层架构只有两个核心组件Provider和Subscribe以及后来的useContainer。Provider的作用极其简单它只是一个包裹器负责将一组 Container 实例注入到 React 组件树的上下文中。它不关心这些 Container 里有什么也不做任何状态合并或派生计算纯粹是“快递员”。Subscribe则是消费者它接收一个 Container 类数组注意是类不是实例并在内部自动创建或复用对应的实例然后将state和setState方法作为 props 传递给子组件。这种设计的精妙之处在于解耦与约定。开发者不需要手动管理 Container 实例的创建时机和生命周期Subscribe会在首次渲染时自动new Container()并在组件卸载时自动清理调用container.destroy()如果定义了的话。这消除了手动useEffect创建/销毁实例的样板代码。更重要的是它强制了一种清晰的依赖声明方式Subscribe to{[CounterContainer, FilterContainer]}这行代码本身就是一份自解释的“依赖清单”任何看到它的人都能立刻明白这个组件依赖哪些状态源。这比在组件内部零散地const counter useContainer(CounterContainer)然后const filter useContainer(FilterContainer)更具可读性和可维护性。它把“依赖注入”这个概念用最符合 React 思维的方式具象化了。3. 核心细节解析与实操要点从安装到生产级落地的全链路3.1 安装与基础集成三步走零配置开箱即用Unstated 的安装和集成是它早期广受欢迎的关键原因之一。整个过程干净利落没有任何构建配置需要调整。第一步安装依赖# 使用 npm npm install unstated # 或使用 yarn yarn add unstated注意这里安装的是unstated而不是unstated-next。后者是社区在 Unstated 停更后为适配 React 16.8 Hooks 而做的现代化 fork功能更强大API 更简洁。但本篇聚焦于标题所指的原版unstated其核心思想完全一致只是 API 略有差异。对于新项目我强烈建议直接使用unstated-next但理解原版是掌握其精髓的基础。第二步创建你的第一个 Container这是最关键的一步也是最容易出错的地方。新手常犯的错误是把Container当成一个普通的工具函数来写忽略了它的类特性。请务必记住Container必须继承自unstated.Container。state必须是一个对象字面量或一个返回对象的函数推荐前者更直观。所有修改状态的方法必须使用this.setState()而不是this.state {...}。后者不会触发更新。// src/containers/TodoContainer.js import { Container } from unstated; class TodoContainer extends Container { // ✅ 正确state 是一个对象 state { todos: [], filter: all // all, active, completed }; // ✅ 正确方法内使用 this.setState addTodo (text) { const newTodo { id: Date.now(), text, completed: false }; this.setState({ todos: [...this.state.todos, newTodo] }); }; // ✅ 正确方法可以是箭头函数确保 this 绑定 toggleTodo (id) { this.setState({ todos: this.state.todos.map(todo todo.id id ? { ...todo, completed: !todo.completed } : todo ) }); }; } export default TodoContainer;第三步在应用根部包裹 Provider并在组件中消费// src/index.js import React from react; import ReactDOM from react-dom/client; import { Provider } from unstated; import TodoContainer from ./containers/TodoContainer; import App from ./App; const root ReactDOM.createRoot(document.getElementById(root)); root.render( Provider inject{[new TodoContainer()]} App / /Provider );注意inject属性它接收一个Container 实例数组而不是类数组。new TodoContainer()这行代码至关重要它创建了状态的“单一事实来源”。如果你在这里传入TodoContainer类本身Provider会报错。App /及其所有子组件现在都处于这个Provider的作用域内可以安全地订阅TodoContainer。3.2 Subscribe 组件的高级用法超越基础消费的灵活性Subscribe组件远不止于简单的状态读取。它的设计提供了多种模式来适应不同的 UI 结构和数据需求。模式一直接消费Props 注入这是最常用、最直观的模式。Subscribe将state和setState方法作为 props 直接传递给子组件。// src/components/TodoList.js import { Subscribe } from unstated; import TodoContainer from ../containers/TodoContainer; const TodoList ({ todos, filter, addTodo, toggleTodo }) { const filteredTodos todos.filter(todo { if (filter active) return !todo.completed; if (filter completed) return todo.completed; return true; }); return ( div input placeholderAdd a new todo... onKeyPress{(e) e.key Enter addTodo(e.target.value) (e.target.value )} / ul {filteredTodos.map(todo ( li key{todo.id} onClick{() toggleTodo(todo.id)} span style{{ textDecoration: todo.completed ? line-through : none }} {todo.text} /span /li ))} /ul /div ); }; // ✅ 关键将 TodoList 包裹在 Subscribe 中 export default () ( Subscribe to{[TodoContainer]} {({ state, setState }) TodoList {...state} {...setState} /} /Subscribe );这里...setState是一个技巧setState是一个函数但Subscribe会将其解构为一个对象其中包含了所有你在 Container 中定义的setState方法如addTodo,toggleTodo。这是一种便捷的写法但要注意如果 Container 中的方法名与state中的属性名冲突可能会覆盖。模式二高阶组件HOC模式提升复用性当你有一组组件都需要访问相同的状态时可以将Subscribe封装成一个 HOC避免在每个组件里重复写Subscribe。// src/hoc/withTodo.js import { Subscribe } from unstated; import TodoContainer from ../containers/TodoContainer; export const withTodo (Component) { return (props) ( Subscribe to{[TodoContainer]} {({ state, setState }) Component {...props} {...state} {...setState} /} /Subscribe ); }; // 在其他组件中使用 // src/components/TodoStats.js import { withTodo } from ../hoc/withTodo; const TodoStats ({ todos }) { const activeCount todos.filter(t !t.completed).length; return pActive: {activeCount}/p; }; export default withTodo(TodoStats);这种模式让状态消费变得像connect一样极大地提升了组件的可测试性和可移植性。你可以轻松地将一个纯函数组件TodoStats注入任何你需要的状态而无需修改其内部逻辑。模式三组合多个 Container构建复杂状态图谱现实中的应用很少只有一个状态源。Unstated 天然支持多 Container 订阅这让你可以构建一个清晰的状态图谱。// src/containers/UserContainer.js import { Container } from unstated; class UserContainer extends Container { state { user: null, loading: false }; login async (credentials) { this.setState({ loading: true }); try { const user await api.login(credentials); this.setState({ user, loading: false }); } catch (err) { this.setState({ loading: false }); throw err; } }; } export default UserContainer;// src/components/Dashboard.js import { Subscribe } from unstated; import TodoContainer from ../containers/TodoContainer; import UserContainer from ../containers/UserContainer; const Dashboard ({ todos, user, login }) { return ( div h1Welcome, {user?.name || Guest}!/h1 TodoList todos{todos} / {!user button onClick{() login({ email: ab.com, password: 123 })}Login/button} /div ); }; // ✅ 同时订阅两个 Container export default () ( Subscribe to{[TodoContainer, UserContainer]} {({ state: todoState, setState: todoSetState, ...userProps }) ( Dashboard {...todoState} {...todoSetState} {...userProps} / )} /Subscribe );Subscribe的to属性接受一个数组它会将所有 Container 的state和setState方法合并后传递给子函数。这种组合方式比在组件内部多次调用useContainer更加扁平和高效。3.3 状态持久化与初始化让状态跨越页面刷新Unstated 本身不提供状态持久化功能但这恰恰是它设计上的高明之处——它把选择权交给了开发者。你可以根据项目需求灵活地在 Container 的生命周期中集成localStorage、sessionStorage或 IndexedDB。在 Container 构造函数中恢复状态// src/containers/PersistentCounterContainer.js import { Container } from unstated; class PersistentCounterContainer extends Container { constructor() { super(); // ✅ 从 localStorage 恢复状态 const saved localStorage.getItem(counter-state); if (saved) { try { this.state JSON.parse(saved); } catch (e) { console.warn(Failed to parse saved counter state, e); this.state { count: 0 }; } } else { this.state { count: 0 }; } } // ✅ 在每次 setState 后保存到 localStorage setState (partialState) { // 先调用父类的 setState触发更新 super.setState(partialState); // 再保存到 localStorage const newState { ...this.state, ...partialState }; localStorage.setItem(counter-state, JSON.stringify(newState)); }; } export default PersistentCounterContainer;这个例子展示了如何在不侵入业务逻辑的前提下无缝集成持久化。setState方法被重写它首先执行父类的更新逻辑然后执行额外的保存逻辑。所有调用this.setState()的地方都会自动触发保存开发者完全无感知。这是一种典型的“横切关注点”Cross-Cutting Concern的优雅实现。注意事项与避坑指南提示localStorage的值是字符串JSON.parse可能抛出异常务必用try/catch包裹。 注意不要在setState中直接修改this.state否则会导致状态不一致。始终使用super.setState()。 注意localStorage有大小限制通常 5MB不适合存储大量数据或二进制内容。4. 实操过程与核心环节实现一个完整的待办事项应用实战4.1 项目结构规划清晰的分层与职责划分一个健壮的 Unstated 应用其目录结构应该清晰地反映出“状态”、“视图”、“逻辑”的分离。我推荐以下结构它经过多个项目验证兼顾了可维护性和可扩展性src/ ├── containers/ # 所有 Container 的定义 │ ├── TodoContainer.js # 核心业务状态 │ ├── FilterContainer.js # 筛选逻辑 │ └── UIContainer.js # 纯 UI 状态如模态框开关、加载状态 ├── components/ # 无状态、可复用的 UI 组件 │ ├── TodoItem.js │ ├── TodoList.js │ └── FilterBar.js ├── hoc/ # 高阶组件用于状态注入 │ └── withContainer.js # 通用的 withContainer HOC ├── App.js # 根组件负责 Provider 包裹 └── index.js # 入口文件这种结构的好处是containers/目录成为了整个应用的“状态中心”任何开发者想了解应用有哪些状态源只需打开这个文件夹。components/目录则专注于 UI 渲染保持高度的纯净和可测试性。hoc/目录则提供了灵活的状态注入方式避免了在每个组件里硬编码Subscribe。4.2 实现 TodoContainer处理增删改查与本地存储让我们动手实现一个功能完备的TodoContainer它将涵盖 CRUD创建、读取、更新、删除的所有操作并集成localStorage。// src/containers/TodoContainer.js import { Container } from unstated; class TodoContainer extends Container { constructor() { super(); // 1. 初始化状态 this.state { todos: [], filter: all, editingId: null, editText: }; // 2. 从 localStorage 恢复 const saved localStorage.getItem(todos-app-state); if (saved) { try { const parsed JSON.parse(saved); // ✅ 只恢复 todos 和 filter忽略 editingId/editTextUI 状态不持久化 this.state { ...this.state, todos: parsed.todos || [], filter: parsed.filter || all }; } catch (e) { console.error(Failed to restore todos from localStorage, e); } } } // 3. 重写 setState实现自动持久化 setState (partialState) { // ✅ 调用父类方法触发 React 更新 super.setState(partialState); // ✅ 只在 todos 或 filter 变化时才保存避免频繁 IO if (todos in partialState || filter in partialState) { const newState { ...this.state, ...partialState }; try { localStorage.setItem(todos-app-state, JSON.stringify(newState)); } catch (e) { console.warn(Failed to save todos to localStorage, e); } } }; // 4. 业务方法添加待办 addTodo (text) { if (!text.trim()) return; const newTodo { id: Date.now(), text: text.trim(), completed: false, createdAt: new Date().toISOString() }; this.setState({ todos: [...this.state.todos, newTodo] }); }; // 5. 业务方法切换完成状态 toggleTodo (id) { this.setState({ todos: this.state.todos.map(todo todo.id id ? { ...todo, completed: !todo.completed } : todo ) }); }; // 6. 业务方法删除待办 deleteTodo (id) { this.setState({ todos: this.state.todos.filter(todo todo.id ! id) }); }; // 7. 业务方法开始编辑 startEditing (id, text) { this.setState({ editingId: id, editText: text }); }; // 8. 业务方法保存编辑 saveEdit (id) { if (!this.state.editText.trim()) return; this.setState({ todos: this.state.todos.map(todo todo.id id ? { ...todo, text: this.state.editText.trim() } : todo ), editingId: null, editText: }); }; // 9. 业务方法取消编辑 cancelEdit () { this.setState({ editingId: null, editText: }); }; // 10. 业务方法清除已完成 clearCompleted () { this.setState({ todos: this.state.todos.filter(todo !todo.completed) }); }; } export default TodoContainer;这个TodoContainer已经是一个生产就绪的模块。它处理了初始化与恢复在构造函数中从localStorage读取。智能持久化重写setState只在关键状态变化时保存避免性能损耗。完整的 CRUDaddTodo,toggleTodo,deleteTodo,startEditing,saveEdit,cancelEdit。UI 状态管理editingId和editText用于控制编辑模式但它们不被持久化因为这是瞬时的 UI 状态而非业务状态。4.3 实现 FilterContainer解耦筛选逻辑提升可测试性将筛选逻辑从TodoContainer中剥离出来是 Unstated 推崇的“单一职责”原则的体现。一个FilterContainer可以被多个组件如TodoList、TodoStats复用而TodoContainer则专注于数据本身。// src/containers/FilterContainer.js import { Container } from unstated; class FilterContainer extends Container { state { filter: all // all, active, completed }; setFilter (filter) { this.setState({ filter }); }; // ✅ 提供一个计算属性方便消费者直接使用 getFilteredTodos (todos) { switch (this.state.filter) { case active: return todos.filter(todo !todo.completed); case completed: return todos.filter(todo todo.completed); default: return todos; } }; } export default FilterContainer;现在在TodoList组件中我们可以这样消费// src/components/TodoList.js import { Subscribe } from unstated; import TodoContainer from ../containers/TodoContainer; import FilterContainer from ../containers/FilterContainer; const TodoList ({ todos, filter, setFilter, getFilteredTodos }) { // ✅ 使用 FilterContainer 提供的计算方法 const filteredTodos getFilteredTodos(todos); return ( div FilterBar filter{filter} setFilter{setFilter} / ul {filteredTodos.map(todo ( TodoItem key{todo.id} todo{todo} / ))} /ul /div ); }; // ✅ 同时订阅两个 Container export default () ( Subscribe to{[TodoContainer, FilterContainer]} {({ state: todoState, ...todoMethods }, { state: filterState, ...filterMethods }) ( TodoList {...todoState} {...todoMethods} {...filterState} {...filterMethods} / )} /Subscribe );这种解耦带来的最大好处是可测试性。你可以单独为FilterContainer编写单元测试验证getFilteredTodos方法在不同filter值下的输出是否正确而无需启动整个 React 应用。这在大型项目中是保障代码质量的基石。4.4 UIContainer管理纯展示性状态保持业务逻辑的纯粹并非所有状态都是业务相关的。像模态框的显示/隐藏、下拉菜单的展开/收起、表单的提交状态loading/success/error等都属于“UI 状态”。将它们与业务状态混在一起会让TodoContainer变得臃肿且难以维护。UIContainer就是为此而生。// src/containers/UIContainer.js import { Container } from unstated; class UIContainer extends Container { state { modalOpen: false, toastMessage: , toastType: info, // success, error, warning isLoading: false }; openModal () this.setState({ modalOpen: true }); closeModal () this.setState({ modalOpen: false }); showToast (message, type info) { this.setState({ toastMessage: message, toastType: type }); // ✅ 3秒后自动关闭 setTimeout(() { this.setState({ toastMessage: }); }, 3000); }; setLoading (isLoading) this.setState({ isLoading }); } export default UIContainer;在App.js中我们将所有 Container 一次性注入// src/App.js import React from react; import { Provider } from unstated; import TodoContainer from ./containers/TodoContainer; import FilterContainer from ./containers/FilterContainer; import UIContainer from ./containers/UIContainer; import TodoApp from ./components/TodoApp; function App() { return ( Provider inject{[ new TodoContainer(), new FilterContainer(), new UIContainer() ]} TodoApp / /Provider ); } export default App;现在任何一个组件都可以按需订阅这些 Container。例如一个Toast组件只需要UIContainer// src/components/Toast.js import { Subscribe } from unstated; import UIContainer from ../containers/UIContainer; const Toast ({ toastMessage, toastType }) { if (!toastMessage) return null; return ( div className{toast toast-${toastType}} {toastMessage} /div ); }; export default () ( Subscribe to{[UIContainer]} {({ state }) Toast {...state} /} /Subscribe );这种分层让TodoContainer保持了绝对的“业务纯洁性”它只关心“待办事项是什么”而不关心“待办事项列表要不要加个 loading 动画”。这种清晰的边界是大型应用可维护性的核心。5. 常见问题与排查技巧实录从新手踩坑到老司机排障5.1 “状态更新了但 UI 没变”—— 最高频的渲染失效问题这个问题几乎是所有 Unstated 新手必经的“洗礼”。原因通常有三个按出现频率排序原因一忘记在 Provider 中注入 Container 实例这是最愚蠢也最常见的错误。你写了new TodoContainer()但忘了把它放进Provider的inject数组里。// ❌ 错误Provider 没有注入任何实例 Provider App / /Provider // ✅ 正确必须注入实例 Provider inject{[new TodoContainer()]} App / /Provider排查技巧打开浏览器的 React DevTools检查Provider组件的props。你应该能看到inject属性下有一个数组里面是你创建的 Container 实例。如果inject是空数组或undefined问题就在这里。原因二在 Container 方法中直接修改this.stateUnstated 的setState是一个异步、批处理的操作。如果你绕过它直接赋值React 就无法感知到变化。// ❌ 错误直接修改 state 对象 this.state.todos.push(newTodo); // 这不会触发更新 // ✅ 正确必须使用 setState this.setState({ todos: [...this.state.todos, newTodo] });排查技巧在 Container 的方法中console.log(this.state)。如果日志显示状态已经变了但 UI 没变那几乎可以断定是直接修改了state。setState是唯一的、受控的更新入口。原因三Subscribe的子函数没有返回 JSX或者返回了null/undefinedSubscribe的子函数是一个 render prop它必须返回有效的 React 元素。// ❌ 错误子函数没有返回值 Subscribe to{[TodoContainer]} {({ todos }) { console.log(todos); // 日志能打印但 UI 是空白的 }} /Subscribe // ✅ 正确必须返回 JSX Subscribe to{[TodoContainer]} {({ todos }) ( div{todos.length} items/div )} /Subscribe排查技巧在Subscribe的子函数开头加一个console.log(rendering)。如果这个日志没打印说明Subscribe根本没被调用检查Provider是否包裹正确如果日志打印了但 UI 没变检查返回值。5.2 “多个 Subscribe 导致性能下降”—— 理解更新粒度与优化策略当一个组件需要订阅多个 Container或者一个 Container 被大量组件订阅时性能问题就会浮现。根本原因还是在于Subscribe的更新机制。问题本质Subscribe组件内部使用forceUpdate来触发重渲染。每当它所订阅的任何一个 Container 调用setStateSubscribe就会强制自己及其所有子组件重新渲染。如果Subscribe包裹了一个庞大的组件树这会造成巨大的浪费。解决方案一精细化订阅用useContainer替代Subscribeunstated-next提供了useContainerHook它比Subscribe更加精细。useContainer可以让你在函数组件内部只订阅你需要的特定 Container并且它的更新是基于useEffect的依赖数组粒度更细。// ✅ 使用 unstated-next 的 useContainer (推荐) import { useContainer } from unstated-next; import TodoContainer from ../containers/TodoContainer; const TodoStats () { const { todos } useContainer(TodoContainer); // ✅ 只订阅 todos 字段 const activeCount todos.filter(t !t.completed).length; return pActive: {activeCount}/p; };useContainer的优势在于它只会在todos数组的引用发生变化时才触发TodoStats的重渲染而不是每次TodoContainer的任何setState都触发。解决方案二拆分Subscribe让每个Subscribe只包裹最小的 UI 单元不要用一个Subscribe包裹整个页面而是让它尽可能靠近需要状态的叶子组件。// ❌ 不推荐大范围包裹 Subscribe to{[TodoContainer, FilterContainer]} {({ todos, filter, setFilter }) ( div Header / FilterBar filter{filter} setFilter{setFilter} / TodoList todos{todos} / Footer / /div )} /Subscribe // ✅ 推荐精细化包裹 div Header / Subscribe to{[FilterContainer]} {({ filter, setFilter }) FilterBar filter{filter} setFilter{setFilter} /} /Subscribe Subscribe to{[TodoContainer]} {({ todos }) TodoList todos{todos} /} /Subscribe Footer / /div这样当FilterContainer更新时只有FilterBar会重渲染当TodoContainer更新时只有TodoList会重渲染。Header和Footer完全不受影响。5.3 “如何在非 React 环境中使用 Container”—— 超越 UI 的状态管理Unstated 的 Container 本质上是一个 JavaScript 类它不依赖于 React。这意味着你可以把它用作一个纯粹的、可测试的状态管理器甚至在 Node.js 环境中使用。场景一单元测试你可以直接new一个 Container 实例然后调用它的方法断言state的变化完全不需要 React 测试库。// __tests__/TodoContainer.test.js import TodoContainer from ../containers/TodoContainer; test(addTodo adds a new todo with correct properties, () { const container new TodoContainer(); container.add