Zustand 也是一款 React 生态中比较流行的状态管理工具,学习成本低,全面拥抱 hooks。
快速上手
首先安装一下依赖(在原有项目上添加)
$ pnpm add zustand
然后编写一个 Todo 组件
import { create } from 'zustand'
const useTodo = create(() => ({
todoList: [
{
id: '1',
title: '早睡'
}
],
}))
export default function Todo() {
const todos = useTodo(state => state.todoList)
return (
<ul>
{ todos.map(todo => (<li key={todo.id}>{todo.title}</li>)) }
</ul>
)
}
可以看到我们预设的 todo 数据已经展示出来了。
create
方法用于创建一个 Store Hook,接收一个函数参数,执行时会向函数中注入 set
方法用于后续Action中更新状态。
现在我们就有了一个只读的 Todo 组件了,这个组件展示了 Zustand 的数据读取能力,下面利用 Zustand 的更新数据的能力来实现添加功能。
import { create } from 'zustand'
import { v4 as UUIDV4 } from 'uuid';
const useTodo = create((set) => ({
todoList: [
{
id: '1',
title: '早睡',
status: 'processing',
}
],
addTodoItem: (title: string) => {
set((state) => ({
todoList: [
...state.todoList,
{
id: UUIDV4(),
title,
status: 'new',
}
]
}))
}
}))
export default function Todo() {
const todoList = useTodo(state => state.todoList)
const addTodoItem = useTodo(state => state.addTodoItem)
return (
<>
<form
onSubmit={(e) => {
// 阻止默认事件(表单提交会跳转页面)
e.preventDefault()
const form = new FormData(e.currentTarget)
const content = form.get('title') as string
// 添加 todo
addTodoItem(content)
// 重置
e.currentTarget.reset()
}}
>
<input type="text" name="title" />
<input type="submit" value="添加" />
</form>
<ul>
{todoList.map(todo => (<li key={todo.id}>{todo.title}</li>))}
</ul>
</>
)
}
此时就有了一个可以添加代办项的 Todo 组件
set
函数会将传入的参数合并到初始状态对象中,如果过程中依赖当前的状态可以将set的设置为函数,执行时会将当前state注入参数,这点跟React setState是一样的。
其他功能就不再一一实现了,方法都类似,看到这里 Zustand 最核心的用法就已经掌握了,没错就是这么简单。
进阶使用
使用 Immer 中间价简化深层属性更新
当存在复杂对象嵌套时,修改深层属性时会比较麻烦,例如这样的数据
const useDeep = create<State>((set) => ({
deep: {
nested: {
otherProp: 'other',
obj: { count: 1 }
}
},
normalInc: () =>
set((state) => ({
deep: {
nested: {
obj: {
count: state.deep.nested.obj.count + 1
}
}
}
})),
}))
export default function Deep() {
const inc = useDeep((state) => state.normalInc)
const deep = useDeep((state) => state.deep)
return (
<div onClick={inc}>{JSON.stringify(deep)}</div>
)
}
在更新时如果只是单纯的更新某个状态会导致其他状态丢失,如下
如果想要保证数据不丢失需要把所有的不变的属性全部拷贝一份。
normalInc: () =>
set((state) => ({
deep: {
...state.deep,
nested: {
...state.deep.nested,
obj: {
...state.deep.nested.obj,
count: state.deep.nested.obj.count + 1
}
}
}
})),
Zustand官方已经考虑到这这个问题,可以使用 Immer 中间件来简化操作,直接修改原对象即可。
(Immer 中间件底层依赖 Immer,所以需要安装一下Immer
$ pnpm add immer
然后修改 set 方法,不需要在返回一个新的值,直接编辑原始对象即可。
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
const useCountStore = create()(immer((set) => ({
deep: {
nested: {
otherProp: 'other',
obj: { count: 0 }
},
},
inc: () =>
set((state) => {
state.deep.nested.obj.count++
}),
})))
然后只更新相关深层属性时就不会丢失数据了。
devtools
Zustand 支持通过 Redux Devtools来可视化调试数据,只需要使用devtools中间件处理即可
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
const useTodo = create()(devtools((set) => ({
todoList: [
{
id: '1',
title: '早睡',
status: 'processing',
}
],
addTodoItem: (title: string) => {
set((state) => ({
todoList: [
...state.todoList,
{
id: UUIDV4(),
title,
status: 'new',
}
]
}))
}
})))
然后就可以在Redux Devtools中看见了
更多第三方中间件可以参考文档。
槽点
Zustand 的类型推导并不是很友好,需要手动声明 Store 的类型,例如这样
interface ITodo {
todoList: Array<{
id: string,
title: string,
status: 'new' | 'processing' | 'done',
}>,
addTodoItem: (title: string) => void
}
const useTodo = create<ITodo>((set) => ({
todoList: [
{
id: '1',
title: '早睡',
status: 'processing',
}
],
addTodoItem: (title: string) => {
set((state) => ({
todoList: [
...state.todoList,
{
id: UUIDV4(),
title,
status: 'new',
}
]
}))
}
}))
否则就会像这样出现类型报错