Zustand 是目前生态系统中非常流行的一款状态管理库,相比经典的 Redux,学习成本极低,使用超简单,今天就来介绍。
快速开始
创建项目
npm create vite@latest zustand-demo -- --template react
cd zustand-demo
# VS Code 打开
code .
npm install zustand
npm install
快速使用 Zustand
删除 src/index.css 中的内容,将 src/App.jsx 中的内容替换如下:
import { create } from 'zustand'
const useStore = create(() => ({
who: 'World',
}))
function App() {
const who = useStore(state => state.who)
return (
<>
Hello {who}!
</>
)
}
export default App
以上,我们就写了一个最小化的 Zustand 应用了。
- 首先,我们向 Zustand 暴露出来的 create() API 中传入了一个回调函数
- 接着,回调函数返回了一个对象
{ who: 'World' }
,这就是状态对象了,那如何用呢?这就德通过返回值了 - 对,没错,create() 会返回一个 React Hook,通常我们会叫它,useStore() 或者是根据具体业务场景而命名的 useXXStore()
- 最后,调用 useStore() 时,Zustand 会将返回的状态传入,由此你可解构出你需要的部分返回。本例中,我们就从 Store 中解构出了
who
这个状态属性
浏览器访问 http://localhost:5173/ 查看:
发现,值为 'World'
的 who
状态被成功渲染出来了。
更新状态
当然除了存储状态,Zustand 还支持我们修改状态,在以上案例的基础之上,我们需要修改 2 处。
const useStore = create((set /* 1 */) => ({
who: 'World',
setWho: (who) => set({ who }) /* 2 */
}))
- 首先,在调用 create() API 的时候,Zustand 还会额外传入一个 set 参数,它是一个函数,用于设置状态
- create() 回调函数中除了能返回状态,还能返回操作状态的函数。本例中新增了一个 setWho() 函数,用于修改状态 who
接下来,稍稍修改 App 组件。
function App() {
const who = useStore(state => state.who)
const setWho = useStore(state => state.setWho) /* 1 */
return (
<>
<p>Hello {who}!</p>
<button onClick={() => setWho('Zustand') /* 2 */}>Say Hi to Zustand</button>
</>
)
}
- 引入状态修改函数 setWho()
- 增加按钮,点击时调用 setWho('Zustand'),将状态 who 更新为
'Zustand'
来看效果:
发现状态被成功修改了。
除此之外,Zustand 还天然支持局部状态更新——就是说如果你的 Store 对象中包含不止一个属性时,你也只需要更新你关心的状态即可,其他状态会自动保持原样。
举个例子。上面的例子,现在除了 who,还有一个 count。
const useStore = create((set) => ({
+ count: 0,
who: 'World',
setWho: (who) => set({ who })
}))
上述的,setWho() 无需改变,保持 set({ who }) 即可,没被设置的 count 依然保持原样。
- <p>Hello {who}!</p>
+ <p>Hello {who}!(<strong>{useStore(state => state.count)}</strong>)</p>
查看效果:
发现 who 被成功更新的同时,count 状态也保留着,这就很棒了。
同样的更新方式,换成 useState(),我们就得这么做:
const [store, setStore] = useState({ who: 'World', count: 0 })
const setWho = (who) => {
setStore((prevStore) => ({
...prevStore,
who: 'Zustand'
}))
}
看看,是不是复杂很多,这就是使用 Zustand 的好处。
另外,set() 函数还支持回调函数更新形式,回调函数会接受当前的状态对象,这样就可以基于以前的状态进行更新了。
以以下新建的 useCountStore 为例:
const useCountStore = create((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}))
更新 count 状态的 inc() 函数内部就是通过在之前 state.count 之上加 1 的方式实现计数增加的。
App 组件修改如下:
function App() {
const count = useCountStore(state => state.count)
const inc = useCountStore(state => state.inc)
return (
<>
<button onClick={() => inc()}>count: {count}</button>
</>
)
}
查看效果:
计数按照预期的增加了。
更新嵌套状态
值得注意的是,Zustand 的局部更新只适应于第一层属性,对嵌套对象中的属性是无效的。
我们先看个反例:
const useCountStore = create((set) => ({
nested: { other: 'other', count: 0 },
inc: () =>
// × 这么做是错的
set((state) => ({
nested: { count: state.nested.count + 1 },
})),
}))
更新 App 组件,再看看效果。
function App() {
const nested = useCountStore(state => state.nested)
const inc = useCountStore(state => state.inc)
return (
<>
<p>{JSON.stringify(nested)}</p>
<button onClick={() => inc()}>inc</button>
</>
)
}
效果:
发现嵌套对象中的 other 属性不见了。
正确的做法应该是这样:
set((state) => ({
- nested: { count: state.nested.count + 1 },
+ nested: { ...state.nested, count: state.nested.count + 1 },
}))
再来看看效果:
这样就没有问题了。
不过,这样更新嵌套对象的方式,着实有些麻烦,如果嵌套层级过深,写出来的代码就极其的丑陋。
// × 不要这么做!
normalInc: () =>
set((state) => ({
deep: {
...state.deep,
nested: {
...state.deep.nested,
obj: {
...state.deep.nested.obj,
count: state.deep.nested.obj.count + 1
}
}
}
})),
这个 Zustand 作者也帮我们想到了,提供了 Immer middleware 帮我们做这件事。
Immer middleware 直接依赖 immer,因此我们还需要安装 immer。
npm install immer
接着,项目中引入:
import { create } from 'zustand'
+ import { immer } from 'zustand/middleware/immer'
此 immer 非彼 immer,middleware/immer 是在 immer 之上的一层封装,为了更好地跟 Zustand 在一起协作。
还是以上方的 useCountStore() 为例。
const useCountStore = create((set) => ({
nested: { other: 'other', count: 0 },
inc: () =>
set((state) => ({
nested: { ...state.nested, count: state.nested.count + 1 },
})),
}))
我们只做 2 件事。
- create() 回调函数采用 immer 包裹
- set() 回调函数内部没有返回值,直接针对 state 进行修改
const useCountStore = create(/* 1 */immer((set) => ({
nested: { other: 'other', count: 0 },
inc: () =>
set((state) => {
state.nested.count++ /* 2 */
}),
})))
查看效果:
跟以前一样。
一旦接入 immer,那么状态更新可以统一改成直接修改 state 的方式,这样更加一致和易于维护。
总结
本文介绍了 Zustand 状态库的简单使用。讲述的内容已经能覆盖大多数的使用场景了。
希望本文的介绍能对你的工作有所帮助,感谢阅读,再见。