Zustand 使用优化:深入探讨状态管理性能提升
细粒度订阅:只订阅你需要的状态
在 Zustand 中,默认情况下,当状态发生变化时,所有订阅该 store 的组件都会重新渲染。但在实际场景中,我们可能只需要部分状态的变化触发组件更新。为了解决这个问题,Zustand 提供了细粒度订阅的方式,通过 useStore
钩子实现。
示例代码:
javascript
const useStore = createStore({
count: 0,
name: 'Alice'
});
function Component() {
const count = useStore(state => state.count); // 只订阅 count
const name = useStore(state => state.name); // 只订阅 name
return (
<div>
<p>Count: {count}</p>
<p>Name: {name}</p>
</div>
);
}
解释:
- 通过
useStore(state => state.xxx)
的方式,组件只订阅特定的状态部分。 - 这种方式避免了因其他状态变化导致的不必要的组件重新渲染。
- 特别适用于需要独立控制不同状态更新的场景。
使用 createWithEqualityFn
优化状态更新
默认情况下,Zustand 的状态更新是通过浅比较(shallow comparison)来判断是否需要重新渲染组件。但在某些复杂场景中,这种比较方式可能不够精确,导致不必要的渲染。这时,我们可以使用 createWithEqualityFn
提供自定义的等式函数。
示例代码:
javascript
const useStore = createWithEqualityFn(
(set) => ({
items: [],
addNewItem: (item) => set((state) => ({ items: [...state.items, item] }))
}),
(prev, next) => {
// 自定义等式函数,仅比较 items 数组的变化
return prev.items === next.items;
}
);
functionListComponent() {
const items = useStore(state => state.items);
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
解释:
createWithEqualityFn
的第二个参数是一个自定义的等式函数,用于判断状态是否发生变化。- 在此示例中,只有当
items
数组发生变化时,组件才会重新渲染。 - 这种方式适用于需要精确控制状态更新的场景,避免因浅比较导致的误判。
createWithEqualityFn + 细粒度订阅优化
未优化的代码
jsx
import create from 'zustand'
const useStore = create(set => ({
count: 0,
inc: () => set(state => ({ count: state.count + 1 }))
}))
使用时
jsx
const Counter = () => {
const { count, inc } = useStore()
return (
<div>
<p>{count}</p>
<button onClick={inc}>+</button>
</div>
)
}
上述代码中,count
改变后,Counter
会重渲染,但如果使用了更多字段,任意一个字段变化都会触发整组件渲染 ------ 性能隐患。
进行优化
jsx
import { createWithEqualityFn } from 'zustand/traditional'
const useStore = createWithEqualityFn(set => ({
count: 0,
name: 'zustand',
inc: () => set(state => ({ count: state.count + 1 }))
}))
使用时只监听 count
:
tsx
const Counter = () => {
const count = useStore(state => state.count)
return <p>{count}</p>
}
这样做可以确保组件只在 count
变化时才更新,不依赖对象整体变更。
细粒度订阅原理与优化方式
Zustand 提供了灵活的 selector 支持,我们可以通过传入函数来选择性订阅状态的某一部分,避免组件对整个状态对象订阅,提高性能。
使用 selector + equalityFn 避免不必要更新
tsx
const useStore = createWithEqualityFn((set) => ({
user: { name: 'John', age: 30 },
updateName: (name: string) => set(state => ({ user: { ...state.user, name } }))
}))
tsx
const UserName = () => {
const name = useStore(state => state.user.name)
return <div>{name}</div>
}
如果没有 createWithEqualityFn,哪怕只更新了 age,也会导致 name 组件重渲染。
使用 Immer 管理不可变状态
Zustand 的状态管理基于不可变更新的原则。为了简化不可变状态的管理,我们可以结合 Immer 库。Immer 提供了一个 produce
方法,使得不可变更新变得简单直观。
示例代码:
javascript
import produce from 'immer';
const useStore = createStore((set) => ({
user: {
name: 'Alice',
age: 25
},
updateUser: (updates) =>
set((state) =>
produce(state, (draft) => {
Object.assign(draft.user, updates);
})
)
}));
function Profile() {
const user = useStore(state => state.user);
const updateUser = useStore(state => state.updateUser);
return (
<div>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
<button
onClick={() => updateUser({ age: 26 })}
style={{ cursor: 'pointer' }}
>
Increment Age
</button>
</div>
);
}
解释:
- 使用 Immer 的
produce
方法,我们可以直接在草案(draft)上进行修改,而无需手动创建新对象。 - 这种方式简化了不可变更新的实现,同时避免了手动操作带来的潜在错误。
- 特别适合处理嵌套较深或复杂的对象结构。
使用 subscribeWithSelector
实现精准订阅
除了 useStore
钩子,Zustand 还提供了 subscribeWithSelector
方法,允许我们在订阅时使用选择器(selector),从而进一步优化状态更新。
subscribeWithSelector
中间件支持精确地监听 store 中某一部分的变化,适用于非组件环境中,如:日志收集、外部状态联动等。
示例代码:
tsx
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
const useStore = create(
subscribeWithSelector((set) => ({
theme: 'light',
toggleTheme: () => set(state => ({ theme: state.theme === 'light' ? 'dark' : 'light' }))
}))
)
监听某一状态
tsx
useEffect(() => {
const unsub = useStore.subscribe(
state => state.theme,
(theme, prev) => {
console.log(`Theme changed from ${prev} to ${theme}`)
}
)
return unsub
}, [])
该方式不会触发组件渲染,适用于全局监听场景。
最佳实践
多 store 模式下的优化建议
在大型项目中推荐将不同逻辑拆分为多个 store,减少无关组件的耦合。
tsx
// useUserStore.ts
export const useUserStore = createWithEqualityFn(set => ({
user: null,
setUser: (user) => set({ user })
}))
// useThemeStore.ts
export const useThemeStore = createWithEqualityFn(set => ({
theme: 'light',
toggle: () => set(state => ({ theme: state.theme === 'light' ? 'dark' : 'light' }))
}))
这样就可以做到组件级别状态订阅颗粒度精细,同时每个 store 的逻辑独立、便于维护与测试。
封装 hooks 与 selector 提升复用性
在大型项目中推荐封装状态 hooks,避免散落式使用 useStore(state => state.xxx)
,便于维护、切换 store 结构时更少代码改动。
tsx
// store/user.ts
import { createWithEqualityFn } from 'zustand/traditional'
export const useUserStore = createWithEqualityFn((set) => ({
user: { name: '', age: 0 },
setName: (name: string) => set((state) => ({
user: { ...state.user, name }
}))
}))
tsx
// hooks/useUserName.ts
import { useUserStore } from '@/store/user'
export const useUserName = () =>
useUserStore((state) => state.user.name)
tsx
// pages/Profile.tsx
import React from 'react'
import { useUserName } from '@/hooks/useUserName'
const Profile = () => {
const name = useUserName()
return <div>User: {name}</div>
}
export default Profile
可在后续替换 store 实现时仅修改 hooks 层即可