React 状态库 Zustand 入门教程

1,526 阅读4分钟

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 应用了。

  1. 首先,我们向 Zustand 暴露出来的 create() API 中传入了一个回调函数
  2. 接着,回调函数返回了一个对象 { who: 'World' },这就是状态对象了,那如何用呢?这就德通过返回值了
  3. 对,没错,create() 会返回一个 React Hook,通常我们会叫它,useStore() 或者是根据具体业务场景而命名的 useXXStore()
  4. 最后,调用 useStore() 时,Zustand 会将返回的状态传入,由此你可解构出你需要的部分返回。本例中,我们就从 Store 中解构出了 who 这个状态属性

浏览器访问 http://localhost:5173/ 查看:

发现,值为 'World'who 状态被成功渲染出来了。

更新状态

当然除了存储状态,Zustand 还支持我们修改状态,在以上案例的基础之上,我们需要修改 2 处。

const useStore = create((set /* 1 */) => ({
  who: 'World',
  setWho: (who) => set({ who }) /* 2 */
}))
  1. 首先,在调用 create() API 的时候,Zustand 还会额外传入一个 set 参数,它是一个函数,用于设置状态
  2. 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>
    </>
  )
}
  1. 引入状态修改函数 setWho()
  2. 增加按钮,点击时调用 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 件事。

  1. create() 回调函数采用 immer 包裹
  2. set() 回调函数内部没有返回值,直接针对 state 进行修改
const useCountStore = create(/* 1 */immer((set) => ({
  nested: { other: 'other', count: 0 },
  inc: () =>
    set((state) => {
      state.nested.count++ /* 2 */
    }),
})))

查看效果:

跟以前一样。

一旦接入 immer,那么状态更新可以统一改成直接修改 state 的方式,这样更加一致和易于维护。

总结

本文介绍了 Zustand 状态库的简单使用。讲述的内容已经能覆盖大多数的使用场景了。

希望本文的介绍能对你的工作有所帮助,感谢阅读,再见。