服务端2026年5月30日 01:39
如何优化 Zustand 状态更新性能?Zustand 性能优化先看订阅粒度:组件只订阅自己需要的字段,不要 `useStore()` 拿整个 store。多个字段一起取时用 shallow 或拆成多个 selector;状态太大时按领域拆 store;异步更新用函数式 `set` 或 `get()` 避免旧值。真正的瓶颈通常不是 Zustand,而是选择器返回新对象、组件订阅过宽、列表渲染太重。
## 追问
### 为什么 `useStore()` 容易造成重渲染?
它订阅整个 store,任何字段变化都会让组件重新渲染。字段越多,误伤越明显。
### shallow 能解决什么问题?
selector 返回对象或数组时,每次都是新引用。shallow 会比较第一层字段,字段没变就不触发更新。
### 拆 store 一定更好吗?
不一定。强相关状态放一起更好维护;变化频率差异很大、业务边界清楚时再拆,否则会增加同步成本。
### 批量更新要手动处理吗?
React 18 下大多数场景会自动批处理。更重要的是把相关字段放在一次 `set` 里,避免中间状态被订阅者看到。
## 写段代码
```javascript
import { shallow } from 'zustand/shallow';
const count = useStore((s) => s.count);
const inc = useStore((s) => s.inc);
const userView = useStore(
(s) => ({ name: s.user.name, role: s.user.role }),
shallow
);
const useStore = create((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 }))
}));
```标签
Zustand
Zustand 是一个简单、快速、可扩展的状态管理库,用于 React 和 React Native 应用程序。它提供了一种创建全局状态的简便方法,而无需过多地关注 Redux 或 Context API 的复杂性。Zustand 的核心概念是创建一个存储(store),其中包含了应用程序的状态和可变更该状态的函数。

服务端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:40
Zustand 有哪些常用中间件?怎么用?Zustand 中间件就是高阶函数,用函数组合串联:create(devtools(persist(immer((set) => ...)))) 从内到外依次包裹。常用 5 个:persist 持久化到 storage;immer 支持 mutable 写法更新嵌套对象;devtools 接入 Redux DevTools;subscribeWithSelector 精确订阅子属性变化;combine 合并多个 slice。
## 追问
### 中间件的执行顺序有讲究吗?
有。从内到外:最内层的先执行。devtools(persist(immer(...))) 意味着 immer 先处理,再 persist,最后 devtools 记录。顺序反了会出错。
### immer 和普通 set 的性能差异?
immer 用 Proxy 追踪变更,有额外开销但通常可忽略。大多数项目 immer 的开发体验收益远大于性能损失。
### 自定义中间件怎么做日志?
拦截 set 参数,前后打印状态即可。生产环境用 devtools 替代手动日志。
### persist 的 partialize 怎么用?
partialize: (state) => ({ token: state.token, theme: state.theme }) 只持久化指定字段,避免把临时 UI 状态也存进 storage。
### subscribeWithSelector 解决什么问题?
默认 useStore(s => s.items) 用 Object.is 比较,对象每次都是新引用所以总是重渲染。subscribeWithSelector 让你可以用 shallow 比较或自定义 equalityFn。服务端5月29日 22:40
Zustand 中如何用 TypeScript 确保类型安全?定义 interface StoreState 声明所有状态和 action 类型,create<StoreState>()((set) => ...) 传入泛型。关键:set((state) => ({ count: state.count + 1 })) 这种函数式更新需要泛型才能正确推断 state 类型。Action 也写在 interface 里,类型签名一目了然。
## 追问
### set 的类型怎么写才不报错?
set 接受 Partial<StoreState> | ((state: StoreState) => Partial<StoreState>)。用 immer middleware 时写法是 set((state) => { state.count += 1 }),不需要返回值。
### selector 类型怎么保证?
useStore(s => s.count) 自动推断为 number。复杂 selector 用 shallow 比较避免重复渲染。不要写 useStore<any> 丢失类型。
### persist middleware 的泛型怎么传?
persist<StoreState> 单独传泛型,和 create 的泛型一致。漏传泛型会导致 set 内部类型丢失。
### 多个 slice 怎么组织类型?
按功能拆分文件,每个 slice 导出自己的 interface 和 create,主 store 用组合模式合并。
### 和 Jotai 的类型体验比呢?
Jotai 的 atom 天然类型安全,不需要额外泛型。Zustand 需要手动传泛型但更灵活。两者都能做到完全类型安全。服务端5月29日 22:40
Zustand 和 Redux 有什么区别?选哪个?核心区别:Zustand 不需要 Provider 包裹、不需要 reducer/action 模板代码、store 直接用 set 修改。Redux 需要 Provider + createSlice + useDispatch + useSelector 四件套。Zustand 1KB vs Redux+RTK 30KB。选型:新项目/中小团队选 Zustand,已有 Redux 代码库或需要强约束选 Redux。
## 追问
### Zustand 没有 DevTools 吗?
有。import { devtools } from zustand/middleware,用法和 Redux DevTools 一样,时间旅行、action 日志都支持。
### Redux 的 middleware 体系 Zustand 怎么替代?
Zustand 用函数组合替代:create(devtools(persist(immer((set) => ...)))) 一行串联。异步不需要 thunk/saga,直接在 action 里 async/await。
### 大型项目 Redux 更稳吗?
Redux 的约束在大型团队中减少出错概率。但 Zustand 配合 TypeScript + selector 约定也能达到类似效果。关键是团队规范。
### 能混用吗?
可以但不推荐——两套模式增加认知负担。迁移建议:新模块用 Zustand,旧模块保持 Redux,逐步替换。
### Immer middleware 和 RTK 的区别?
都是用 Immer 实现不可变更新,语法几乎一致,底层都是 produce。区别只在包装层。服务端5月29日 22:40
React Native 中如何使用 Zustand 管理状态?和 Web 端完全一样——npm install zustand,create((set) => ({ ... })) 创建 store,组件里 useStore(selector) 读取。Zustand 不依赖 DOM API,纯 JS 实现,React Native 直接能用。唯一需要注意的是持久化:Web 用 localStorage,RN 用 mmkv 或 AsyncStorage,配合 zustand/middleware 的 persist 中间件传入不同的 storage adapter。
## 追问
### Zustand 和 Redux 在 RN 中哪个更合适?
Zustand。RN 对包体积敏感,Zustand 1KB vs Redux+RTK 约 30KB。API 更简洁,不需要 Provider 包裹根组件。新项目优先 Zustand。
### 如何做持久化存储?
用 MMKV 而非 AsyncStorage——MMKV 同步读写,快 30 倍。persist 中间件传入 createJSONStorage(() => mmkvStorage) 即可。关键字段(token、用户偏好)必须持久化。
### 多个页面共享状态会重复渲染吗?
用 selector 精确订阅就不会。useStore(s => s.token) 只在 token 变化时重渲染。切忌 useStore() 全量订阅。
### 后台被系统杀掉 store 会丢吗?
没做持久化的会丢。RN 端内存回收后 JS 上下文重建,store 恢复初始值。关键字段必须 persist。
### 导航中需要传递 store 吗?
不需要。Zustand 是全局单例,任何组件直接 useStore 即可,不用像 Context 那样逐层传递。服务端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' }
)
)
```