ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Refine 生态系列:Zustand 状态管理实战指南——用 Hooks 驱动轻量级全局状态

Refine 生态系列:Zustand 状态管理实战指南——用 Hooks 驱动轻量级全局状态 Refine 生态系列Zustand 状态管理实战指南——用 Hooks 驱动轻量级全局状态【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine本篇技术指南以 Refine 文档站点中的 Ecosystem / Integrations 博客文章 为骨架系统讲解 Zustand 这一基于 Hooks 的轻量级 React 全局状态管理库从核心设计理念、安装配置、create/set等基础 API到防重复渲染的 memoization 技巧、异步 Action、嵌套状态结构、中间件与自定义 Hooks 等进阶用法并辅以一个完整可运行的 To-do 应用实战。读完本文你将掌握在不引入 Provider、零样板代码的前提下用 Zustand 为 React含 Refine 构建的内部工具、Admin 面板应用组织可预测、可维护的全局状态。为什么关注 Zustand从 Redux 说起在全局状态管理领域Redux 曾长期占据主导地位它强大的可预测性、便捷的调试体验以及广泛应用的最佳实践使其成为几乎所有开发者接触全局状态的第一站。与此同时Redux 的上手成本与样板代码也一直为人诟病——创建 store、包裹 Context Provider、在组件中引入useSelector/useDispatch等步骤缺一不可。Zustand 正是在这一背景下出现的新面孔。它被定位为一个小型、快速、可扩展的极简状态管理方案基于简化的 Flux 原则实现提供基于 Hooks 的舒适 API且不强制约定、不裹挟样板代码。它由 Jotai 和 React-spring 背后的团队构建开源且拥有活跃的社区维护。与本仓库的生态系列文章React 生态系统 2024中的描述一致Zustand 是一款非常轻量的 React 状态管理库遵循 Flux 原则打包体积小内置 TypeScript 支持可与 Immer.js 等第三方库配合创建不可变状态。需要注意的是当前 refine 仓库根目录的 package.json 与 pnpm-lock.yaml 中并未把 zustand 列为直接依赖它属于文档生态中介绍的第三方集成方案可自由选装。Zustand 的核心设计理念要真正用好 Zustand需要先理解它与 Redux 的几个关键差异无需 Provider 包裹Zustand 不像 React-Redux 那样要求你用 Content Provider 包裹整个应用而是主要通过 Hooks 与状态通信结构上更加扁平。单一数据源与状态切片State SlicesZustand 拥抱单一数据源single source of truth理念整个应用状态集中存储在一个 store 中。store 由多个状态切片构成——每个切片代表应用的不同部分可拆分为独立的 store 定义从而实现模块化与相关状态/动作的封装。默认不可变Immutability by Default更新状态时创建新的状态对象而非直接修改旧对象这简化了状态管理、避免了常见的变更类 bug也让 React 的变更检测与重渲染更加高效。订阅与选择性响应Subscriptions Selective Reactivity组件可以订阅特定的状态切片仅在切片变化时自动重渲染。Zustand 基于代理proxy实现了细粒度的依赖追踪从而高效更新、最小化不必要的重渲染。更少样板代码同样的计数器状态用 Redux 需要创建 store、包裹 Provider、引入useSelector/useDispatch等一整套流程而 Zustand 只需一个create调用。环境准备与安装Zustand 是运行在 Node.js 环境下的 JavaScript 库使用前需确保本机具备以下基础工具Node.js提供运行时与npmnpm 随 Node.js 二进制包一起安装无需单独安装。npm 或 yarn包管理器用于维护和管理项目依赖。创建一个新的 Node.js 项目并安装 Zustandmkdir zustand-prj cd zustand-prj npm init --ymkdir zustand-prj在当前目录创建名为zustand-prj的新目录。cd zustand-prj将当前工作目录切换到zustand-prj。npm init --y初始化新的 Node.js 项目并生成package.json清单文件--y表示自动接受全部默认选项。安装 Zustand 库npm install zustand # 或使用 yarn yarn add zustand创建第一个 Store认识 create 与 set使用 Zustand 的第一步是从主入口导入create函数import { create } from zustand;create接收一个回调函数并返回一个自定义 Hook。在这个回调函数里我们定义状态以及操作状态的方法——状态与方法都以对象形式从回调中返回。create会把一个set函数传给回调set正是用来修改 store 中状态的核心工具const useCounter create((set) { return { counter: 0, incrCounter: () set((state) ({ counter: state.counter 1 })), }; });在上述例子中store 里有两个成员状态counter和动作incrCounter。useCounter是一个自定义 Hook可在任意组件中使用并获取最新状态。假如组件 A、B、C 都使用了该 HookB 中对状态所做的任何修改都会反映到 A 和 C 中它们会重新渲染以呈现最新变化。Zustand 中的状态可以是原始值primitive、对象或函数。这一点从源码结构的组织方式也能印证状态与动作被统一放进 store 对象中动作本质上就是状态的一部分。在组件中使用 Store选择切片与读取全量状态create返回的自定义 Hook 用法与 React-Redux 中的useAppSelector类似——你调用 Hook 并传入一个回调函数Hook 内部会调用该回调并把当前状态传给它你在回调中返回想要的那部分状态即切片const counter useCounter((state) state.counter);将切片选择与展示组件结合const DisplayCounter () { const counter useCounter((state) state.counter); return divCounter: {counter}/div; };再创建一个负责递增状态的组件——注意它与展示组件彼此独立仅仅通过 store 共享状态const CounterControl () { const incrCounter useCounter((state) state.incrCounter); return ( div button onClick{incrCounter}Incr. Counter/button /div ); };把两个组件组装进Appconst App () { return ( DisplayCounter / CounterControl / / ); };每当点击CounterControl中的按钮DisplayCounter就会重新渲染出最新的counter值——两个组件零耦合却由 Zustand 神奇地连接在一起。这正是 Zustand 相对 React-Redux 的优势所在同样的状态逻辑Redux 需要创建 store、包裹 Provider、逐一引入useSelector/useDispatch而 Zustand 将其大大简化。返回整个状态当调用create返回的 Hook 时不传任何回调Hook 将返回 store 的整个状态const state useCounter();此时可以像访问普通对象一样读取与调用state.counter; // 0 state.incrCounter(); // 1防止不必要的重渲染memoization、shallow 与自定义比较器memoization 是一种优化技术通过缓存耗时函数调用的结果当相同参数再次调用时直接返回缓存值从而避免重复计算、提升性能。Zustand 允许我们为自定义 Hook 添加 memoization——它导出了一个shallow函数用于对状态切片做浅比较import { shallow } from zustand/shallow;仍以useCounter为例假设counter初始值为 0当通过incrCounter更新后DisplayCounter会重渲染但如果更新后的值仍为 0例如并发场景下值未实际变化这次重渲染就是不必要的。如何阻止这种前后值相等却仍然重渲染的情况Zustand 的做法是给自定义 Hook 传入第二个参数——一个比较器函数comparator。它比较上一次的状态切片与下一次的状态切片相同则不重渲染不同则重渲染。这与useEffect、useMemo、useCallback等 React Hooks 的依赖比较思想一脉相承。shallow就是 Zustand 内置的比较器它使用浅相等比较两个状态切片const counter useCounter((state) state.counter, shallow);如果你不信任shallow的判定也可以编写自定义比较器。比较器接收两个参数——上一次的切片值、下一次的切片值(previousState, nextStateSlice) 在函数体内完成比较并返回结果返回true让 Hook 跳过重渲染返回false则触发重渲染。为counter切片编写一个基于的比较器(previousCounter, nextCounter) previousCounter nextCounter;把它接入useCounterconst counter useCounter( (state) state.counter, (previousCounter, nextCounter) previousCounter nextCounter, );至此useCounter已具备 memoization 能力不再有无谓的重渲染应用运行更高效。补充一点版本提示较新版本v5也支持直接从主入口import { create, shallow } from zustand引入shallow两种写法按你所用的版本选择即可。更新状态set 的两种形态与清空状态此前只讨论了从 store 读取状态这一节聚焦如何更新状态。set函数支持两种调用形态。形态一传入回调函数推荐用于依赖当前值const useCounter create((set) { return { counter: 0, incrCounter: () set((state) ({ counter: state.counter 1 })), }; });set会调用这个回调并把当前状态作为参数传入然后用回调的返回值更新状态。注意回调返回的对象中携带counter属性set据此知道要更新 state 中的哪些属性——只更新返回对象中出现的属性其余属性保持不变。形态二直接传入对象set({ counter: 9, });这会将counter状态直接更新为 9。清空整个状态向set传入空对象并带上第二个参数trueset({}, true);这会清除 store 中的状态与动作。认识 Action同步与异步Action 是什么Action 是 store 状态中那些用于改变 store 的函数类似于 React-Redux 中的 dispatch action。例如上面的incrCounter就是一个 Action——它在内部调用set来更新counter状态。异步 ActionZustand 并不关心你的 Action 是同步还是异步——异步函数天然受支持。例如可以在 Action 中发起 HTTP 请求并用返回结果更新状态const useCounter create((set) { return { counter: 0, incrCounter: async () { const { data } await axios.get(/counter); set({ counter: data.counter, }); }, }; });这里用async关键字把incrCounter声明为异步函数内部请求/counter接口后通过set更新counter。需要说明的是示例中的axios并非 Zustand 的一部分只是演示异步取数 更新状态的常见组合实际项目中可替换为你偏好的请求库。实战用 Zustand 构建 To-do 应用掌握了基础 API 之后我们动手构建一个由 Zustand 驱动状态管理的 To-do 应用。首先用create-react-app脚手架创建 React 项目并安装 Zustandcreate-react-app todo-app cd todo-app npm install zustand1. 创建 Storeimport { create } from zustand; const useStore create((set) ({ todos: [], addTodo: (text) set((state) ({ todos: [ ...state.todos, { id: Date.now(), text, completed: false, }, ], })), toggleTodo: (id) set((state) ({ todos: state.todos.map((todo) todo.id id ? { ...todo, completed: !todo.completed } : todo, ), })), deleteTodo: (id) set((state) ({ todos: state.todos.filter((todo) todo.id ! id), })), })); export default useStore;Store 中有一个todos数组状态以及三个 ActionaddTodo追加新待办用Date.now()生成 id、toggleTodo切换某条待办的completed标记、deleteTodo从数组中移除指定 id 的待办。注意这里全部通过展开旧数组/旧对象生成新引用的方式更新保持了 Zustand 的不可变更新约定。2. 列表展示组件 DisplayTodosconst DisplayTodos () { const { todos, toggleTodo, deleteTodo } useStore((state) { return { todos: state.todos, toggleTodo: state.toggleTodo, deleteTodo: state.deleteTodo }; }); return ( ul {todos.map((todo) ( li key{todo.id} style{{ textDecoration: todo.completed ? line-through : none, }} onClick{() toggleTodo(todo.id)} {todo.text} button onClick{() deleteTodo(todo.id)}Delete/button /li ))} /ul ); }; export default DisplayTodos;这里从状态中切出todos数组用Array#map渲染列表同时切出toggleTodo点击待办切换完成态已完成项显示删除线与deleteTodo点击 Delete 按钮按 id 移除该项。实用提示原文档示例中 onClick 使用了未从 store 切出的toggleTodo这里已补充切出使代码可直接运行。3. 新增待办组件 TodosControlconst TodosControl () { const addTodo useStore((state) state.addTodo); const [text, setText] useState(); function handleSubmit(e) { e.preventDefault(); addTodo(text); setText(); } return ( form onSubmit{handleSubmit} input typetext value{text} onChange{(e) setText(e.target.value)} / button typesubmitAdd/button /form ); }; export default TodosControl;组件通过useState维护输入框文本text表单提交时触发handleSubmit调用addTodo(text)创建新待办并清空输入框。4. 组装应用const App () { return ( DisplayTodos / TodosControl / / ); }; export default App;至此一个增、删、勾选完整的 To-do 应用仅靠一个 store 与几个组件即告完成——没有 Provider、没有 reducer、没有 dispatch这正是 Zustand 极简 风格的最佳体现。进阶管理嵌套状态结构面对复杂的状态结构Zustand 支持创建嵌套的状态切片使状态组织更模块化更新某一小块状态时不波及其他部分const useStore create((set) ({ user: { name: , age: 0, address: { street: , city: , }, }, updateUser: (newUser) set((state) ({ user: { ...state.user, ...newUser } })), updateAddress: (newAddress) set((state) ({ user: { ...state.user, address: { ...state.user.address, ...newAddress }, }, })), }));user下嵌套了address子结构因此分别提供updateUser与updateAddress两个方法前者浅合并用户字段后者只浅合并地址字段其余层级原样保留——通过逐层展开spread保证不可变更新。使用中间件devtools 与更多能力Zustand 可以通过中间件middleware增强功能例如记录状态变更日志、在 localStorage 中保存/恢复状态、处理异步动作等。以日志/调试最常用的devtools为例只需用中间件包裹 store 定义import { create } from zustand; import { devtools } from zustand/middleware; const useStore create( devtools((set) ({ count: 0, increment: () set((state) ({ count: state.count 1 })), })), );包裹devtools后状态变更便可在 Redux DevTools 扩展中观察这对调试大有裨益。除devtools外Zustand 还提供persist持久化到 localStorage 等存储介质、immer以可变写法生成不可变更新等中间件它们都从zustand/middleware子路径导入。编写自定义 Hooks 与工具函数封装状态逻辑为自定义 Hook自定义 Hook 可以将 Zustand 的状态逻辑封装起来让复用与复杂状态交互的管理更轻松。以用户认证状态为例import { create } from zustand; const useAuthStore create((set) ({ user: null, login: (userData) set({ user: userData }), logout: () set({ user: null }), })); const useAuth () { const { user, login, logout } useAuthStore(); return { user, login, logout }; }; export default useAuth;之后任何组件只需调用useAuth()即可获得user、login、logout认证状态逻辑被收敛到一处代码更整洁、更易复用。构造工具函数简化深层更新工具函数可以简化状态更新逻辑、提升代码可读性。下面用点路径dot path定位并更新深层嵌套状态const updateNestedState (set, keyPath, value) { set((state) { const keys keyPath.split(.); let nestedState state; keys.slice(0, -1).forEach((key) { nestedState nestedState[key]; }); nestedState[keys[keys.length - 1]] value; return { ...state }; }); }; const useNestedStateStore create((set) ({ data: { user: { profile: { name: , }, }, }, updateProfileName: (name) updateNestedState(set, data.user.profile.name, name), }));通过updateNestedState(set, data.user.profile.name, name)一行即可更新任意深度的状态属性该工具函数可在应用各处复用。值得注意的是这里为了演示点路径遍历直接修改了nestedState的叶子属性最终通过返回{ ...state }生成新引用——在生产代码中推荐仍以不可变方式逐层 spread完成深更新避免共享引用引发副作用。与 Refine 项目结合的建议Refine 作为构建内部工具、Admin 面板与 B2B 应用的数据驱动框架其自身的 Hooks如useList、useForm、useShow等已接管了大部分与数据提供者data provider相关的服务端状态而 Zustand 这类轻量客户端状态库天然适合管理与服务端无关的 UI 状态——例如主题偏好、侧边栏折叠状态、筛选条件的临时草稿、全局通知配置等。将两者结合时建议遵循各司其职的分工服务端数据交给 Refine 的 Hooks 与缓存机制纯客户端瞬时状态交给 Zustand避免两套方案在职责上重叠。相关的基础背景也可参考仓库中的 React 生态系统 2024 系列文章 中对 Zustand 特性的概括介绍。总结回顾本文的探索路径我们先从 Redux 的痛点切入认识了 Zustand 的设计理念与差异化优势随后完成环境准备与安装掌握了create/set基础 API学会在组件中选择状态切片、读取全量状态接着通过shallow与自定义比较器实现了防重复渲染的 memoization深入理解了set的对象/函数两种更新形态、清空状态、Action 与异步 Action再以一个完整的 To-do 应用串联全部知识点最后进阶到嵌套状态结构、devtools中间件、自定义 Hooks 与工具函数。正如原文档所强调的这些只是 Zustand 能力的冰山一角——它极简、基础、有趣而更深层的能力持久化、Immer 集成、暂存状态、跨 store 通信等依然有待探索。在 Refine 这样的数据驱动框架中将服务端数据交给 Refine、客户端瞬时状态交给 Zustand是一条清晰且低成本的集成路径。【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表