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 数据已经展示出来了。

EE04AEC1-F5FD-48A2-BDD3-CC6F97FCC82F-13990-00000973C31D8893

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 组件

B011753C-C303-48F2-9B5E-AC5DBBEB25A0-13990-00000973C8631AFD

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>
  )
}

在更新时如果只是单纯的更新某个状态会导致其他状态丢失,如下

38703FFF-4F83-4B34-A1AF-5D598D44DEF0-13990-00000973D0D174A2

如果想要保证数据不丢失需要把所有的不变的属性全部拷贝一份。

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++
    }),
})))

然后只更新相关深层属性时就不会丢失数据了。

8A85818A-DAD1-42CD-AA1F-873CCA542569-13990-00000973D4F9AA6D

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中看见了

image-20240724211054744

更多第三方中间件可以参考文档

槽点

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',
        }
      ]
    }))
  }
}))

否则就会像这样出现类型报错

6C6192B4-1E8E-4E5B-B9FB-A55125B57D40-13990-00000973CC741C2A


前端小白