服务端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),其中包含了应用程序的状态和可变更该状态的函数。

服务端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 });
}
}
}));
```服务端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 });
});
```服务端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' }
)
)
```服务端5月29日 01:38
Zustand 中如何用 TypeScript 定义 Store 类型?定义一个包含状态和方法的 interface,作为 create 的泛型参数传入即可:create<StoreState>((set, get) => ({...}))。TypeScript 会自动推断 set 回调中 state 的类型,方法参数也能正确约束。关键点:状态和方法写在同一个 interface 中,方法参数可引用自身类型(如 setUser: (user: StoreState['user']) => void)。
## 追问
**中间件会改变 store 类型,怎么处理类型组合?**
persist/devtools 等中间件会向 store 注入额外属性(如 persist.clearStorage),需定义扩展类型 type Store = StoreState & StorePersist,再用 create<Store>() 包裹。zustand v4+ 的中间件泛型签名已支持自动推断,大部分场景无需手动拼接。
**StateCreator 泛型怎么用?**
StateCreator<StoreState, Mutators, U> 用于拆分 store 逻辑时约束每个切片的类型,Mutators 描述中间件链对 set/get 的改写,U 是未包装的原始类型。手动组合切片时需要用到。
**selector 的返回类型怎么保证?**
useStore(s => s.count) 自动推断为 number。自定义 selector 返回派生值时,TypeScript 能推断返回类型;如果需要显式标注,写 useStore<number>((s) => s.items.length)。
**泛型 store 怎么定义?**
function createSlice<T>() 返回 StateCreator<SliceState<T>>,通过泛型参数让切片适配不同实体(如 createSlice<User>()、createSlice<Order>()),避免为每个实体写重复逻辑。
**set 的 partial 参数类型为什么不是 Partial<StoreState>?**
set 接受 Partial<StoreState> 或 (state) => Partial<StoreState>,但嵌套对象是浅合并,深层更新需展开:set(s => ({ user: { ...s.user, name: 'new' } })),否则丢失同级字段。
## 写段代码
```ts
interface BearState {
bears: number
inc: (n: number) => void
}
const useBearStore = create<BearState>((set) => ({
bears: 0,
inc: (n) => set((s) => ({ bears: s.bears + n })),
}))
// 组件中
const bears = useBearStore((s) => s.bears) // number
```服务端5月29日 01:38
Zustand 是什么?相比 Redux 有什么优势?Zustand 是一个极简 React 状态管理库,核心 API 只有 create():传入一个返回状态对象的函数,返回一个 hook,组件通过 const count = useStore(s => s.count) 按需订阅,未变化的部分不会触发重渲染。不需要 Provider 包裹,不需要 reducer/action 分发,setState 直接更新。体积约 1KB,零依赖。
## 追问
**Zustand 的 selector 如何避免不必要的重渲染?**
useStore(selector) 只订阅 selector 返回的切片,内部用 Object.is 浅比较判断是否变化。引用类型可传第二个参数 shallow 比较函数,或用 useShallow。
**和 Jotai/Recoil 的原子化方案有什么区别?**
Zustand 是单一 store(可拆分),状态集中管理;Jotai/Recoil 是原子化,每个状态独立。Zustand 更适合关联性强的状态,原子化适合独立派生状态。
**没有 Provider 怎么实现组件间共享状态?**
Zustand store 本质是一个模块级闭包,所有引用同一 useStore 的组件共享同一个状态引用,不依赖 React Context 传递。
**create() 返回的 hook 能在组件外使用吗?**
可以。useStore.getState() 获取快照、useStore.setState() 更新状态,均可在非组件代码(如 axios 拦截器、WebSocket 回调)中使用,这是相比 useContext 的显著优势。
**多 store 还是单 store?**
Zustand 不限制,实践中按领域拆分多个 store 更常见,避免单个 store 臃肿,也便于按需加载和测试。
## 写段代码
```ts
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}))
function Counter() {
const count = useStore((s) => s.count)
return <button onClick={useStore.getState().inc}>{count}</button>
}
```服务端5月28日 02:37
如何创建和使用 Zustand store?## 核心答案
Zustand 通过 `create` 函数创建 store,返回一个可直接在组件中使用的 Hook。与 Redux 不同,它不需要 Provider 包裹,store 本身就是 Hook:
```javascript
import { create } from 'zustand'
const useStore = create((set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
reset: () => set({ count: 0 }),
}))
```
组件中使用时,推荐通过选择器订阅,避免不必要的重渲染:
```javascript
const count = useStore((state) => state.count) // 只订阅 count
const increment = useStore((state) => state.increment) // 只订阅 increment
```
## set 与 get 的用法
`set` 用于更新状态,支持对象和函数两种形式。Zustand 自动浅合并第一层属性,所以不需要手动展开 `...state`:
```javascript
const useStore = create((set) => ({
user: { name: 'Tom', age: 20 },
// 对象形式:直接替换第一层属性
setName: (name) => set({ user: { name, age: 20 } }), // 注意:第二层需手动处理
// 函数形式:基于旧状态计算
incrementAge: () => set((state) => ({
user: { ...state.user, age: state.user.age + 1 }
})),
}))
```
`get` 用于在 action 中读取当前状态,不触发订阅:
```javascript
const useStore = create((set, get) => ({
items: [],
addItem: (item) => set({ items: [...get().items, item] }),
getCount: () => get().items.length, // 不触发重渲染
}))
```
## 选择性订阅与性能优化
直接解构整个 store 会导致任何状态变化都触发重渲染,应避免:
```javascript
// 不推荐:任何状态变化都触发重渲染
const { count, name } = useStore()
// 推荐:按需订阅
const count = useStore((s) => s.count)
const name = useStore((s) => s.name)
```
对于复杂对象,使用 `shallow` 比较避免引用变化导致的重渲染:
```javascript
import { shallow } from 'zustand/shallow'
const { name, age } = useStore(
(s) => ({ name: s.user.name, age: s.user.age }),
shallow
)
```
## Store 拆分(Slice 模式)
大型应用中,将不同领域的状态拆成独立 slice,再合并到一个 store:
```javascript
// slices/cartSlice.js
export const createCartSlice = (set) => ({
items: [],
addItem: (item) => set((s) => ({ items: [...s.items, item] })),
clearCart: () => set({ items: [] }),
})
// slices/userSlice.js
export const createUserSlice = (set) => ({
user: null,
setUser: (user) => set({ user }),
})
// store.js
import { create } from 'zustand'
import { createCartSlice } from './slices/cartSlice'
import { createUserSlice } from './slices/userSlice'
const useStore = create((...a) => ({
...createCartSlice(...a),
...createUserSlice(...a),
}))
```
## 异步操作
Zustand 的 action 可以直接是 async 函数,不需要额外的中间件:
```javascript
const useStore = create((set) => ({
data: null,
loading: false,
error: null,
fetchData: async (id) => {
set({ loading: true, error: null })
try {
const res = await fetch(`/api/data/${id}`)
const data = await res.json()
set({ data, loading: false })
} catch (error) {
set({ error: error.message, loading: false })
}
},
}))
```
## 常用中间件
### persist — 持久化到 localStorage
```javascript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
const useStore = create(
persist(
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{ name: 'theme-storage' } // localStorage key
)
)
```
### immer — 不可变更新的简化写法
```javascript
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
const useStore = create(
immer((set) => ({
user: { name: 'Tom', address: { city: 'Beijing' } },
setCity: (city) => set((state) => { state.user.address.city = city }),
// 无需手动展开,直接修改 draft
}))
)
```
### devtools — Redux DevTools 调试支持
```javascript
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
const useStore = create(
devtools((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}), { name: 'CounterStore' })
)
```
中间件可以组合使用,顺序从外到内:`devtools(persist(immer(...)))`。
## create 与 createStore 的区别
| | `create` | `createStore` |
|---|---|---|
| 返回值 | React Hook | Store 对象 |
| 使用场景 | React 组件内 | React 外(测试、服务端、非React环境) |
| 订阅方式 | `useStore(s => s.xxx)` | `store.subscribe()` / `store.getState()` |
```javascript
import { createStore } from 'zustand'
const store = createStore((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}))
// React 外部使用
store.getState().count // 读取
store.setState({ count: 10 }) // 更新
store.subscribe((state) => { // 监听
console.log('state changed', state)
})
```
## 追问:Zustand 与 Redux 的核心区别是什么?
1. **无需 Provider**:Zustand 不需要 `<Provider>` 包裹组件树,直接导入 Hook 使用
2. **订阅粒度**:Zustand 通过选择器精确订阅,Redux 用 `useSelector` 实现类似效果但机制不同
3. **样板代码**:Zustand 无 action type、reducer、dispatch,一个函数搞定
4. **Bundle 体积**:Zustand ~1KB vs Redux Toolkit ~11KB
5. **中间件生态**:Redux 有更成熟的中间件链,Zustand 的中间件更轻量但够用服务端5月28日 01:21
如何优化 Zustand 的性能,减少不必要的重渲染?## 核心答案
Zustand 减少不必要重渲染的关键在于**精确订阅**——让组件只在自己关心的状态变化时才重新渲染。具体手段有三层:
1. **选择器订阅**:用 `useStore(state => state.xxx)` 替代 `useStore()` 全量订阅
2. **浅比较防抖**:用 `useShallow` 处理多值订阅,避免对象引用变化导致的误触发
3. **自定义等式函数**:用 `createWithEqualityFn` 或第二个参数实现精确的变更判断
## 为什么选择器能避免重渲染
Zustand 内部基于发布-订阅模式工作。当 `set()` 被调用时,它会遍历所有订阅者,把新状态传给每个选择器函数,然后用 `Object.is` 比较选择器返回值是否变化——只有变化了才会触发对应组件的重渲染。
所以,如果你用 `useStore()` 订阅了整个 store,任何 `set()` 都会让选择器返回一个新对象,`Object.is` 判定不等,组件就重渲染了。这就是全量订阅的问题所在。
## 选择器订阅的正确写法
### 基础:单值订阅
```javascript
import { useStore } from './store';
function Counter() {
// 只订阅 count,user 变化不会触发重渲染
const count = useStore((state) => state.count);
return <span>{count}</span>;
}
```
### 多值订阅:用 useShallow 而非对象选择器
当你需要同时订阅多个值时,一个常见错误是在选择器里返回新对象:
```javascript
// 错误:每次调用都返回新对象引用,Object.is 判定不等,必然重渲染
const { count, name } = useStore((state) => ({
count: state.count,
name: state.name,
}));
```
正确做法是用 `useShallow`:
```javascript
import { useShallow } from 'zustand/react/shallow';
const { count, name } = useStore(
useShallow((state) => ({
count: state.count,
name: state.name,
}))
);
```
`useShallow` 的原理很简单:它用浅比较(shallow equal)对比前后选择器返回的对象每个属性,只有某个属性确实变了才判定为需要更新。也可以拆成多个独立选择器:
```javascript
const count = useStore((state) => state.count);
const name = useStore((state) => state.name);
```
每个选择器独立订阅,互不影响。对于原始类型(string、number、boolean),推荐拆开写,更直观。
## 自定义等式函数
有些场景 `useShallow` 不够用,比如选择器返回数组或需要深层比较时。Zustand 支持传入第二个参数作为自定义比较函数:
```javascript
import { shallow } from 'zustand/shallow';
// 方式一:使用 shallow 工具函数
const items = useStore((state) => state.filteredItems, shallow);
```
或者用 `createWithEqualityFn` 创建 store 时统一指定:
```javascript
import { createWithEqualityFn } from 'zustand/traditional';
import { shallow } from 'zustand/shallow';
const useStore = createWithEqualityFn(
(set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}),
shallow
);
```
## 状态结构设计
扁平化 store 结构能让选择器更精准:
```javascript
// 不推荐:深层嵌套,选择器难以精确订阅
const useStore = create((set) => ({
ui: {
modal: { isOpen: false, content: '' },
toast: { visible: false, message: '' },
},
}));
// 推荐:扁平拆分,各自独立订阅
const useUIStore = create((set) => ({
modalOpen: false,
modalContent: '',
toastVisible: false,
toastMessage: '',
}));
```
也可以把不同关注点拆成多个 store,Zustand 对多 store 没有额外开销。
## 批量更新
Zustand 的 `set()` 本身是同步的,多次 `set` 在同一个事件循环里会触发多次渲染。如果需要批量更新多个字段,合并成一次 `set`:
```javascript
// 不好:两次 set,两次渲染
const updateSeparately = (count, user) => {
set({ count });
set({ user });
};
// 好:一次 set,一次渲染
const updateTogether = (count, user) => {
set({ count, user });
};
```
## 在 React 外使用 getState / setState
事件处理函数、定时器回调、WebSocket 处理器中,如果不需要触发组件重渲染,直接用 `getState` 和 `setState`:
```javascript
// 不触发任何组件重渲染的读写
const currentValue = useStore.getState().count;
useStore.setState({ count: currentValue + 1 });
// 在 React 组件的事件处理器中,也可以直接操作
function handleClick() {
useStore.setState((s) => ({ count: s.count + 1 }));
}
```
这种方式绕过了订阅机制,适合"写了但不关心谁在监听"的场景。
## 常见陷阱
| 陷阱 | 表现 | 修复 |
|------|------|------|
| 选择器返回新对象/数组 | 每次都重渲染 | 用 `useShallow` 或拆成多个选择器 |
| 全量订阅 `useStore()` | 任何状态变化都重渲染 | 用选择器订阅具体字段 |
| 选择器里有复杂计算 | 计算开销大或结果引用不稳定 | 把计算移到 store 里预计算,或用 `useMemo` |
| subscribeWithSelector 配合不当 | 深层比较失效 | 确保选择器返回值可比较 |
## 性能对比参考
在同一基准测试下,Zustand 选择器订阅的重渲染延迟约 0.1-0.5ms,而全量订阅在大型 store 下可达 2-5ms。1000 个列表项更新场景,精确订阅约 14ms,全量订阅约 22ms。这些差距在小型应用中不明显,但在中大型项目中会累积。
## 优化检查清单
- 选择器订阅替代全量订阅
- 多值订阅用 `useShallow` 或拆成独立选择器
- 复杂比较场景用 `shallow` 或自定义等式函数
- 状态结构扁平化,避免深层嵌套
- 多字段更新合并成一次 `set` 调用
- React 外的场景用 `getState` / `setState`服务端5月28日 01:18
如何使用 Zustand 创建和管理全局状态?## 核心答案
Zustand 通过 `create` 函数创建 store,将状态和操作定义在同一对象中,组件通过 hook 选择性订阅所需状态片段,从而实现轻量级的全局状态管理。与 Redux 相比,Zustand 无需 Provider 包裹、无需 reducer/action 分离,store 可在组件外直接调用 `getState()`/`setState()`,且内置 selector 机制避免不必要的重渲染。
## 创建 Store
```typescript
import { create } from 'zustand';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
```
`set` 接受对象或函数:对象形式直接替换字段,函数形式接收当前 state 返回增量更新。两种方式都是浅合并,不会覆盖未提及的字段。
## 组件中使用
```typescript
function Counter() {
// 选择性订阅:仅 count 变化时重渲染
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
return (
<div>
<p>{count}</p>
<button onClick={increment}>+1</button>
</div>
);
}
```
选择订阅是 Zustand 性能优势的核心:只有 selector 返回值变化时组件才会重渲染。直接解构 `const { count } = useCounterStore()` 会导致整个 store 任一字段变化都触发渲染。
## 异步操作与中间件
```typescript
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
const useUserStore = create(
devtools(
persist(
(set) => ({
user: null,
fetchUser: async (id: string) => {
const res = await fetch(`/api/users/${id}`);
const user = await res.json();
set({ user }, false, { type: 'fetchUser' });
},
logout: () => set({ user: null }, false, { type: 'logout' }),
}),
{ name: 'user-storage' } // localStorage 持久化
)
)
);
```
异步操作无需额外处理,在 action 中直接 `await` 后调用 `set` 即可。`persist` 将状态持久化到 storage,`devtools` 接入 Redux DevTools 调试。中间件通过函数组合嵌套,顺序影响行为。
## 组件外访问 Store
```typescript
// 读取当前状态
const currentUser = useUserStore.getState().user;
// 监听状态变化
const unsub = useUserStore.subscribe((state) => {
console.log('user changed:', state.user);
});
// 直接更新状态
useUserStore.setState({ user: null });
```
这一特性使得在工具函数、路由守卫、API 拦截器等非组件场景中也能读写全局状态,是 Zustand 区别于 Context API 的重要优势。
## 追问
1. **Zustand 的 `set` 是浅合并还是替换?能否实现深度合并?** 默认浅合并;深度合并需手动展开 `{ ...state.nested, ...newVal }` 或搭配 `immer` 中间件。
2. **如何拆分大型 Store?** 可以按领域创建多个独立 store(如 `useUserStore`、`useCartStore`),也可以用 `slices` 模式将逻辑拆分后组合为单 store。
3. **Zustand 与 Jotai 的适用场景有何不同?** Zustand 是 store-based,适合管理聚合状态;Jotai 是 atom-based,适合管理细粒度的原子状态,随用随创建。
4. **`useSyncExternalStore` 和 Zustand 的关系?** Zustand v4+ 底层使用 `useSyncExternalStore` 实现与 React 18 并发模式的兼容,确保状态更新在并发渲染中不会出现 tearing。
5. **如何实现 Zustand Store 的按需加载?** 将 store 定义放在动态 import 的模块中,使用时才加载;或用 `lazy` 模式在首次访问时初始化状态。服务端5月28日 01:18
Zustand 与 Redux 相比有哪些优缺点?### Zustand 的核心优势
**极简 API,告别样板代码**
Zustand 创建 store 只需一个函数调用,无需定义 action types、reducers、action creators。与 Redux Toolkit 的 createSlice 相比,代码量减少 60% 以上:
```js
// Zustand — 一个函数搞定 store
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
// 组件中直接用 hook
function Counter() {
const { count, increment } = useStore()
return <button onClick={increment}>{count}</button>
}
```
```js
// Redux Toolkit — 需要更多概念
import { createSlice, configureStore } from '@reduxjs/toolkit'
import { Provider, useSelector, useDispatch } from 'react-redux'
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1 },
decrement: (state) => { state.value -= 1 },
},
})
const store = configureStore({ reducer: { counter: counterSlice.reducer } })
// 还需要 Provider 包裹 + useSelector + useDispatch
function Counter() {
const count = useSelector((s) => s.counter.value)
const dispatch = useDispatch()
return <button onClick={() => dispatch(increment())}>{count}</button>
}
```
**无需 Provider,更干净的组件树**
Redux 必须在应用顶层包裹 `<Provider store={store}>`,导致组件树多出一层嵌套,测试时也需要用 `<Provider>` 包裹。Zustand 直接在模块作用域创建 store,组件通过 hook 消费状态,无需任何包裹组件。这对存量项目集成尤其友好——不用改已有的组件树结构就能接入状态管理。
**更小的体积,更快的加载**
| 库 | Gzipped 大小 |
|---|---|
| Zustand | ~1.1 KB |
| Redux Toolkit | ~8.2 KB |
| Redux Toolkit + React-Redux | ~11.8 KB |
在 3G 网络下,11 KB 的差距可带来 50-100ms 的交互时间优化。对包体积有严格限制的移动端 H5 场景,Zustand 优势明显。
**内置选择器,精准控制重渲染**
Zustand 支持细粒度订阅,只订阅需要的状态切片,自动跳过无关更新:
```js
// 只订阅 count,其他状态变化不会触发重渲染
const count = useStore((state) => state.count)
// 也支持 shallow 比较对象
import { shallow } from 'zustand/shallow'
const { count, name } = useStore(
(state) => ({ count: state.count, name: state.name }),
shallow
)
```
Redux 的 `useSelector` 同样支持选择器,但默认使用 `===` 严格比较,返回对象时需要手动使用 `shallowEqual`,容易遗忘导致不必要的重渲染。
**TypeScript 开箱即用**
Zustand 的 store 定义自带类型推导,无需额外声明类型:
```ts
const useStore = create<{ count: number; increment: () => void }>((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}))
// count 和 increment 类型自动推导,无需泛型
```
Redux Toolkit 虽然也有良好的 TS 支持,但 createSlice 和 configureStore 需要更多类型标注和泛型配置。
**异步操作更直观**
Zustand 处理异步不需要额外中间件,直接写 async 函数:
```js
const useStore = create((set) => ({
data: null,
loading: false,
fetchData: async () => {
set({ loading: true })
const res = await fetch('/api/data')
const data = await res.json()
set({ data, loading: false })
},
}))
```
Redux Toolkit 通过 createAsyncThunk 处理异步,需要定义 pending/fulfilled/rejected 三种状态,样板代码更多。
### Zustand 的不足
**生态和中间件相对薄弱**
Redux 拥有 Redux Saga、Redux Observable、Redux Persist 等成熟中间件生态,处理竞态、取消、重试等复杂副作用有成熟方案。Zustand 自带 persist、devtools、immer 等中间件,覆盖常见需求,但社区中间件数量和成熟度仍远不及 Redux。遇到复杂副作用场景时,往往需要自己实现或组合多个中间件。
**调试体验有差距**
Zustand 可通过 `devtools` 中间件接入 Redux DevTools,但时间旅行调试和 action 回放功能不如原生 Redux 完善。对于需要严格追踪每次状态变更、回溯 bug 的场景,Redux 的调试体验更可靠。
**大型项目实践尚在积累**
Zustand 已被 React Three Fiber、shadcn/ui、Next.js 示例等项目广泛采用,2026 年 npm 周下载量达 700 万次,但在超大型企业级应用中的最佳实践仍不如 Redux 成熟。缺少千人团队的治理模式参考和大规模重构案例。
**灵活性的双刃剑**
Zustand 不强制状态更新模式,不同开发者可能写出风格迥异的 store。在缺乏 Code Review 约束的团队中,灵活反而变成混乱。Redux 的严格单向数据流天然约束了风格统一性,新人接手代码时理解成本更低。
### 什么时候选 Zustand
- **中小型项目**:状态逻辑不复杂,追求开发速度和简洁性
- **存量项目集成**:不想引入 Provider 改动组件树,零侵入接入
- **性能敏感场景**:对包体积和重渲染有严格要求,如移动端 H5
- **快速原型开发**:重视迭代速度而非架构完备性
- **React Three Fiber / 可视化项目**:Zustand 是 R3F 生态的默认选择
### 什么时候选 Redux
- **大型企业级应用**:团队 5 人以上,需要标准化流程和严格约束
- **复杂副作用**:需要 Saga 级别的竞态处理、取消和重试能力
- **重度调试需求**:时间旅行调试对排查线上问题至关重要
- **团队已熟悉 Redux**:迁移成本高于收益,不必为了换而换
- **金融 / 交易类系统**:需要追踪每一次状态变更的审计场景
### 怎么选:一句结论
项目小、求快、求轻选 Zustand;项目大、求稳、求规范选 Redux。两者并非互斥——复杂项目中可以在全局状态用 Redux、局部状态用 Zustand,按需搭配。服务端5月28日 01:18
Zustand 中的 set 函数有几种使用方式?Zustand 的 set 函数有三种使用方式,面试中需要完整回答。
### 1. 对象式更新——直接传入新状态
```javascript
const useStore = create((set) => ({
count: 0,
reset: () => set({ count: 0 })
}));
```
适用于更新不依赖当前状态值的场景,写法简洁。set 会将传入的对象与当前状态**浅合并**,未提及的字段保持不变。
### 2. 函数式更新——基于当前状态计算新值
```javascript
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}));
```
函数接收当前 state,返回要合并的对象。异步操作中必须用这种写法,否则闭包会捕获过期的 state:
```javascript
// 错误:count 来自闭包,可能是旧值
const badIncrement = async () => {
await delay(1000);
set({ count: count + 1 });
};
// 正确:state 始终是最新的
const goodIncrement = async () => {
await delay(1000);
set((state) => ({ count: state.count + 1 }));
};
```
### 3. 替换模式——完全替换而非合并
set 的第二个参数 `replace` 默认为 `false`(浅合并)。设为 `true` 时,传入的对象会**完全替换**当前状态,而非合并:
```javascript
const useStore = create((set) => ({
count: 0,
name: 'demo',
// 只剩 count,name 被清除
resetAll: () => set({ count: 0 }, true)
}));
```
实际开发中常用于登出时清空整个 store,或切换用户时重置所有字段。
### 三种方式的对比
| 方式 | 签名 | 状态处理 | 适用场景 |
|------|------|----------|----------|
| 对象式 | `set(partial)` | 浅合并 | 不依赖旧值的更新 |
| 函数式 | `set((state) => partial)` | 浅合并 | 依赖旧值、异步操作 |
| 替换式 | `set(partial, true)` | 完全替换 | 重置 store |
### 浅合并的行为细节
浅合并只做第一层合并,嵌套对象需要手动展开:
```javascript
const useStore = create((set) => ({
user: { name: 'Tom', age: 20 },
// 错误:age 丢失
badUpdate: () => set({ user: { name: 'Jerry' } }),
// 正确:展开后再覆盖
goodUpdate: () => set((state) => ({
user: { ...state.user, name: 'Jerry' }
}))
}));
```
如果嵌套层级深,可以配合 `immer` 中间件来简化写法。
### 面试追问:set 为什么默认浅合并而不是深合并?
性能。深合并需要递归遍历整个状态树,对大多数场景来说是不必要的开销。浅合并只需一次 `Object.assign` 级别的操作,配合 React 的引用比较就能高效判断是否需要重渲染。需要深合并时,开发者自行选择 immer 等工具即可。服务端5月28日 00:45
什么是 Zustand,它与其他状态管理库相比有哪些优势?## 核心答案
Zustand 是一个极简的 React 状态管理库,gzip 后仅约 1KB。它通过 `create` 函数创建 store,组件用 hook 订阅状态,无需 Provider 包裹,也不需要 reducer/action 等样板代码。与 Redux 相比,Zustand 的 API 更简洁、包体积小 5-7 倍、重渲染速度快 30-50%,已成为 2026 年新 React 项目的首选状态管理方案。
## 基本用法
创建一个 store 只需要调用 `create`:
```js
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
```
组件中使用:
```jsx
function Counter() {
const { count, increment, decrement } = useStore()
return (
<div>
<span>{count}</span>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
</div>
)
}
```
没有任何 Provider、没有 action 类型定义、没有 switch-case reducer。状态更新直接通过 `set` 函数完成。
## 选择性订阅:减少不必要的重渲染
这是 Zustand 的关键优势之一。组件可以只订阅它关心的状态切片:
```jsx
// 只在 count 变化时重渲染,其他状态更新不会触发
const count = useStore((state) => state.count)
// 也可以用 selector 订阅派生状态
const isPositive = useStore((state) => state.count > 0)
```
相比之下,React Context 的消费者在 context 值变化时会全部重渲染,这在大型应用中是性能瓶颈。
## 与其他状态管理库的对比
| 特性 | Zustand | Redux Toolkit | Jotai | Valtio |
|------|---------|---------------|-------|--------|
| 包体积 (gzip) | ~1KB | ~6-8KB | ~3KB | ~3KB |
| 状态模型 | 不可变 | 不可变 | 原子化 | 可变 |
| 需要 Provider | 否 | 是 | 是 | 否 |
| 样板代码量 | 极少 | 中等 | 极少 | 极少 |
| 学习曲线 | 平缓 | 中等 | 平缓 | 平缓 |
| DevTools 支持 | 内置 | 完整 | 基础 | 基础 |
| TypeScript 支持 | 优秀 | 优秀 | 优秀 | 良好 |
**选择建议**:中小型项目、追求简单高效选 Zustand;大型团队需要严格架构和可预测数据流选 Redux Toolkit;需要细粒度原子状态选 Jotai;偏好可变状态写法选 Valtio。
## 中间件:持久化与开发调试
Zustand 的中间件机制让功能扩展非常方便:
```js
import { create } from 'zustand'
import { persist, devtools } from 'zustand/middleware'
const useStore = create(
devtools(
persist(
(set) => ({
theme: 'light',
toggleTheme: () =>
set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light',
})),
}),
{ name: 'theme-storage' }
),
{ name: 'ThemeStore' }
)
)
```
`persist` 将状态自动同步到 localStorage,页面刷新后状态不丢失。`devtools` 集成 Redux DevTools 扩展,方便调试状态变化。中间件可以自由组合,按需叠加。
## 在 Next.js 中使用
Zustand 与 Next.js App Router 配合时,需要注意 store 不能在模块顶层直接创建(服务端会共享状态),推荐使用懒初始化模式:
```jsx
// store.js
import { create } from 'zustand'
const useStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
}))
export default useStore
// layout.jsx 或 page.jsx
'use client'
import useStore from './store'
export default function Page() {
const user = useStore((state) => state.user)
return <div>{user?.name ?? '未登录'}</div>
}
```
关键点是将使用 store 的组件标记为 `'use client'`,store 本身保持普通的模块导出即可。
## 异步操作
Zustand 处理异步不需要额外的中间件(Redux 需要 redux-thunk 或 createAsyncThunk),直接在 `set` 中写 async 函数:
```js
const useStore = create((set) => ({
data: null,
loading: false,
error: null,
fetchData: async (id) => {
set({ loading: true, error: null })
try {
const res = await fetch(`/api/data/${id}`)
const data = await res.json()
set({ data, loading: false })
} catch (error) {
set({ error: error.message, loading: false })
}
},
}))
```
## 追问:什么时候不该用 Zustand?
如果项目只需要组件间传递少量状态,React 自带的 `useState` + `useContext` 就够了,没必要引入外部库。另外,服务端状态(API 请求缓存、分页数据)更适合用 TanStack Query 管理,Zustand 专注的是客户端 UI 状态。两者经常在同一项目中配合使用——TanStack Query 管接口数据,Zustand 管界面交互状态。
前端2月7日 13:42
React 如何在 Class 组件中设置 zustand 状态在类组件中使用 Zustand 状态管理通常不是直接支持的,因为 Zustand 主要是为 React 的函数组件设计的,利用了 React 的钩子(Hooks)系统。然而,你仍然可以在类组件中间接使用 Zustand。
要在类组件中使用 Zustand,你可以创建一个函数组件作为类组件的子组件或高阶组件,这个函数组件可以使用 Zustand 的 `useStore` 钩子来访问和修改状态,然后将状态通过 props 传递给需要的类组件。
下面是具体的实现步骤:
1. **定义 Zustand 的 store**
```javascript
import create from 'zustand';
const useStore = create(set => ({
counter: 0,
increment: () => set(state => ({ counter: state.counter + 1 })),
decrement: () => set(state => ({ counter: state.counter - 1 }))
}));
```
2. **创建一个函数组件来连接 Zustand store 和类组件**
```javascript
import React from 'react';
const WithZustandStore = (Component) => {
return function WrappedComponent(props) {
const { counter, increment, decrement } = useStore();
return <Component
{...props}
counter={counter}
increment={increment}
decrement={decrement}
/>;
};
};
```
3. **在类组件中使用通过 props 传递的 Zustand 状态和方法**
```javascript
import React, { Component } from 'react';
class CounterComponent extends Component {
render() {
const { counter, increment, decrement } = this.props;
return (
<div>
<div>Counter: {counter}</div>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
}
// 使用高阶组件包装类组件
export default WithZustandStore(CounterComponent);
```
在这个例子中,`WithZustandStore` 是一个高阶组件,它接收一个组件作为参数,并返回一个新的组件。这个新组件使用 `useStore` 钩子来访问 Zustand 的状态和方法,并将它们作为 props 传递给原始的类组件。这样,即使在类组件内部,你也可以使用 Zustand 管理的状态。