标签

Zustand

Zustand 是一个简单、快速、可扩展的状态管理库,用于 React 和 React Native 应用程序。它提供了一种创建全局状态的简便方法,而无需过多地关注 Redux 或 Context API 的复杂性。Zustand 的核心概念是创建一个存储(store),其中包含了应用程序的状态和可变更该状态的函数。

Zustand
查看更多相关内容
服务端2026年5月30日 01:39
如何在 Zustand 中处理异步操作?Zustand 处理异步不需要额外机制,直接在 action 里写 `async/await`,用 `set` 更新 `loading/data/error`,需要最新状态时用 `get()`。如果是服务端缓存、重试、失效刷新这类问题,优先交给 React Query 或 SWR,Zustand 只保存跨页面共享的 UI 或业务状态。 ## 追问 ### async action 里为什么要用 `get()`? 异步代码执行时,闭包里的旧值可能已经过期。`get()` 读取的是当前 store 状态,适合在 await 后继续基于最新状态更新。 ### loading 和 error 应该怎么设计? 简单请求可以放 `loading: boolean`;多个并发请求最好按 key 存状态,避免 A 请求结束把 B 请求的 loading 误关掉。 ### Promise 链和 async/await 有区别吗? 能力上差不多。面试回答用 async/await 更清楚,但要说明 action 可以返回 Promise,组件或测试里可以继续 await。 ### 什么时候不该把请求全塞进 Zustand? 需要缓存、分页、去重、后台刷新、请求取消时,不要自己造一套数据请求框架,直接用 React Query/SWR 更稳。 ## 写段代码 ```javascript const useStore = create((set, get) => ({ user: null, loading: false, error: null, fetchUser: async (id) => { set({ loading: true, error: null }); try { const user = await fetch(`/api/users/${id}`).then(r => r.json()); set({ user, loading: false }); } catch (e) { set({ error: e.message, loading: false }); } } })); ```
服务端2026年5月30日 01:39
如何对 Zustand store 进行单元测试?Zustand store 单测重点是把状态恢复到干净初始值,再验证 action、异步状态和 selector 行为。同步 action 可以直接用 `getState()` 调;React hook 场景用 `renderHook` 和 `act`;异步 action 要 mock 请求并等待 Promise 结束。面试里别只说“很好测”,要提到全局 store 会污染用例,必须在 `beforeEach` 重置。 ## 追问 ### 为什么每个测试前要重置 store? Zustand store 默认是模块级单例,上一个测试改过的状态会留到下一个测试,导致用例顺序一变就失败。 ### 测 action 一定要 renderHook 吗? 不一定。纯 store 逻辑用 `useStore.getState().action()` 更快;只有要验证 hook 订阅和组件重渲染时,才需要 `renderHook`。 ### 异步 action 怎么测? mock `fetch` 或请求层,调用 action 后断言 loading、data、error 的最终状态。需要中间态时,可以分阶段 await。 ### selector 性能怎么测? 订阅一个具体 selector,更新无关字段,断言渲染次数不变;再更新目标字段,断言它才重新触发。 ## 写段代码 ```javascript beforeEach(() => { useStore.setState({ count: 0, user: null }, true); }); test('increments count', () => { useStore.getState().increment(); expect(useStore.getState().count).toBe(1); }); test('fetch user', async () => { vi.spyOn(global, 'fetch').mockResolvedValue({ json: async () => ({ id: 1 }) }); await useStore.getState().fetchUser(1); expect(useStore.getState().user).toEqual({ id: 1 }); }); ```
服务端2026年5月30日 01:39
如何在 Zustand 中自定义中间件?Zustand 自定义中间件本质是包一层 `config`:拦截 `set/get/api`,再把增强后的能力交还给 store。常见用途是日志、校验、性能埋点、撤销重做。面试里先说函数签名,再强调两点:不要破坏原始 `set` 语义;组合多个 middleware 时外层先执行,顺序会影响结果。 ## 追问 ### 自定义 middleware 和普通 action 封装有什么区别? 普通 action 只管某个业务动作;middleware 能统一拦截所有状态更新,适合横切能力,比如日志、持久化、校验。 ### `set` 包装时最容易踩什么坑? 别忘了转发 `replace` 参数,也不要在 middleware 里无条件再次调用增强后的 `set`,否则可能递归或改变 replace 行为。 ### 多个 middleware 的执行顺序怎么看? `create(a(b(config)))` 中,`a` 先拿到 config 并包装,实际更新时通常外层逻辑先触发。日志、persist、immer 混用时要明确谁先处理原始对象。 ### 实际项目会怎么用? 我更倾向把日志、权限校验、状态快照放 middleware,业务状态变化仍留在 action 里,避免 middleware 变成黑盒业务层。 ## 写段代码 ```javascript const logger = (config) => (set, get, api) => config((partial, replace) => { const prev = get(); const ret = set(partial, replace); console.log('zustand change', { prev, next: get() }); return ret; }, get, api); const useStore = create(logger((set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 })) }))); ```
服务端5月29日 22:35
Zustand 中如何处理异步操作?Zustand 的 store 就是个普通对象,create 回调里直接写 async 函数即可,不需要 thunk 之类的中间件。写法:在 `create((set, get) => ({ ... }))` 里定义 async action,内部 await 拿到数据后调 `set({ data, loading: false })`。手动管理 loading/error 状态是最常见的方式。如果嫌重复,用 `zustand/middleware` 的 `immer` 简化嵌套更新,或封装一个 `createAsyncAction` 工具函数统一处理 loading/success/error 三态。 ## 追问 ### Zustand 和 Redux Toolkit 处理异步有什么区别? RTK 需要 `createAsyncThunk` + `extraReducers` 处理三态,模板代码多。Zustand 直接在 action 里 await + set,没有中间件概念,代码量少一半以上。RTK 的优势是 DevTools 自动追踪异步状态,Zustand 需要手动 `devtools` middleware。 ### 并发请求怎么处理? 两个独立请求各自 async action 并行调用即可。如果需要等全部完成:`await Promise.all([fetchA(), fetchB()])`。注意竞态问题——快速切换页面时旧请求后到会覆盖新数据,用请求 ID 或 `AbortController` 取消旧请求。 ### Suspense 能配合 Zustand 用吗? 可以,但需要包装成 throw promise 的模式:store 里存 promise 而非数据,组件读时如果 promise 未 resolve 就 throw 出去,Suspense 捕获。推荐用 `use` 包(React 19 内置)或 `suspend-react` 简化。不过大多数项目手动 loading 状态更直观,Suspense 方案适合设计系统级别统一处理。 ### 如何做请求缓存和去重? 简单方案:store 里维护 `Map<cacheKey, { data, timestamp }>`,action 里先查缓存未过期就直接返回。复杂场景用 `SWR` 或 `TanStack Query` 管缓存,Zustand 只管 UI 状态,职责分离更清晰。 ### 服务端渲染(SSR)时异步怎么处理? `create` 时传入 `hydrate` 数据,客户端 `useEffect` 里发起请求覆盖。注意避免服务端和客户端数据不一致的 hydration mismatch——初始渲染用服务端数据,客户端请求完成后 `set` 更新即可。
服务端5月29日 01:38
Zustand 中间件怎么使用?有哪些内置中间件?Zustand 中间件以函数组合方式包裹 create 的回调,从内到外依次嵌套。内置三个核心中间件:persist(状态持久化到 localStorage/sessionStorage)、devtools(接入 Redux DevTools 调试)、immer(简化不可变更新,可直接写 state.user.name = 'new')。组合顺序:devtools 在最外层,persist 在内层,中间件顺序影响 set/get 的拦截链。 ## 追问 **persist 的 partialize 怎么过滤不需要持久化的字段?** partialize: (state) => ({ user: state.user }) 只持久化 user,token 等敏感或临时字段不会写入存储。反序列化时缺失字段会使用 create 中的初始值填充。 **immer 中间件解决了什么问题?** React 要求状态不可变更新,深层嵌套需逐层展开 {...s, user: {...s.user, name: 'new'}},代码冗长易出错。immer 让你直接赋值 state.user.name = 'new',内部通过 Proxy 生成新对象。 **中间件组合顺序有影响吗?** 有。devtools(persist(fn)) 中 persist 在内层,持久化后的状态变化才会被 devtools 捕获;反过来的话 devtools 记录的是持久化前。一般推荐 devtools 在外、persist 在内。 **如何自定义中间件?** 中间件本质是高阶函数:(config) => (set, get, api) => config(fnSet, fnGet, api),在 fnSet/fnGet 中插入自定义逻辑(如日志、节流、权限校验),然后调用原 set/get。 **persist 持久化的状态怎么版本迁移?** persist 配置中提供 migrate 函数:migrate: (persisted, version) => { if (version === 0) return { ...persisted, newField: 'default' }; return persisted; },配合 version 字段标识当前版本,自动执行迁移。 ## 写段代码 ```ts import { create } from 'zustand' import { persist, devtools } from 'zustand/middleware' const useStore = create( devtools( persist( (set) => ({ token: '', setToken: (t: string) => set({ token: t }), }), { name: 'auth', partialize: (s) => ({ token: s.token }) } ), { name: 'AuthStore' } ) ) ```