标签

Zustand

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

Zustand
服务端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: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 管理的状态。