Zustand 状态库:轻便、简洁、强大的 React 状态管理工具

一、Zustand 状态库简介

Zustand 是一个轻量级、简洁且强大的 React 状态管理库,旨在为您的 React 项目提供更简单、更灵活的状态管理方式。与其他流行的状态管理库(如 Redux、MobX 等)相比,Zustand 的 API 更加简洁明了,学习成本较低,且无需引入繁琐的中间件和配置。同时,Zustand 支持 TypeScript,让您的项目更具健壮性。

Zustand 官方文档地址 : docs.pmnd.rs/zustand/get...

zustand 中文网: awesomedevin.github.io/zustand-vue...

提到状态管理,大家可能首先想到的是 redux。

redux 是老牌状态管理库,能完成各种基本功能,并且有着庞大的中间件生态来扩展额外功能。

但 redux 经常被人诟病它的使用繁琐。

近两年,React 社区出现了很多新的状态管理库,比如 zustand、jotai、recoil 等,都完全能替代 redux,而且更简单。

zustand 算是其中最流行的一个。

看 star 数,redux 有 60k,而 zustand 也有 38k 了:

看 npm 包的周下载量,redux 有 880w,而 zustand 也有 260w 了:

从各方面来说,zustand 都在快速赶超 redux。

在公司的项目中使用的是 dva.js 作为 状态管理,但是Dva.js在编写代码时过于臃肿,并且 Dva 不再维护,其在 ts 下的都没有任何提示的问题也逐步暴露。这使得我考虑有没有一种更加优雅的方式进行React状态的管理,并且能够完美兼容项目中已有的状态管理方法,作为一种补充手段为开发提效。

zustand 能完美满足我这一需求,它足够简单且能够和其他状态库共存。

二、Zustand 的优势

  1. 轻量级 :Zustand 的整个代码库非常小巧,gzip 压缩后仅有 1KB,对项目性能影响极小。

  2. 简洁的 API :Zustand 提供了简洁明了的 API,能够快速上手并使用它来管理项目状态。 基于钩子: Zustand 使用 React 的钩子机制作为状态管理的基础。它通过创建自定义 Hook 来提供对状态的访问和更新。这种方式与函数式组件和钩子的编程模型紧密配合,使得状态管理变得非常自然和无缝。

  3. 易于集成 :Zustand 可以轻松地与其他 React 库(如 Redux、MobX 等)共存,方便逐步迁移项目状态管理。

  4. 支持 TypeScript:Zustand 支持 TypeScript,让项目更具健壮性。

  5. 灵活性:Zustand 允许根据项目需求自由组织状态树,适应不同的项目结构。

  6. 可拓展性 : Zustand 提供了中间件 (middleware) 的概念,允许你通过插件的方式扩展其功能。中间件可以用于处理日志记录、持久化存储、异步操作等需求,使得状态管理更加灵活和可扩展。

三、如何在 React 项目中使用 Zustand

1. 安装 Zustand

bash 复制代码
npm install zustand

或者

bash 复制代码
yarn add zustand

2,快速上手

js 复制代码
// 计数器 Demo 快速上手
import React from "react";
import { create } from "zustand";

// create():存在三个参数,第一个参数为函数,第二个参数为布尔值
// 第一个参数:(set、get、api)=>{...}
// 第二个参数:true/false 
// 若第二个参数不传或者传false时,则调用修改状态的方法后得到的新状态将会和create方法原来的返回值进行融合;
// 若第二个参数传true时,则调用修改状态的方法后得到的新状态将会直接覆盖create方法原来的返回值。

const useStore = create(set => ({
  count: 0,
  setCount: (num: number) => set({ count: num }),
  inc: () => set((state) => ({ count: state.count + 1 })),
}));

export default function Demo() {
  // 在这里引入所需状态
  const { count, setCount, inc } = useStore();

  return (
    <div>
      {count}
      <input
        onChange={(event) => {
          setCount(Number(event.target.value));
        }}
      ></input>
      <button onClick={inc}>增加</button>
    </div>
  );
}

3, 在状态中访问和存储数组

假设我们需要在 Zustand 中存储一个 state 中的数组, 我们可以像下面这样定义

ts 复制代码
const useStore = create(set => ({
  fruits: ['apple', 'banana', 'orange'],
  addFruits: (fruit) => {
    set(state => ({
      fruits: [...state.fruits, fruit]
    }));
  }
}));

以上, 我们创建了一个 store 包含了 fruits state, 其中包含了一系列水果, 第二个参数是 addFruits , 接受一个参数 fruit 并运行一个函数来得到 fruits state 和 新增的 fruits, 第二个变量用于更新我们存储状态的值

4,访问存储状态

当我们定义上面的状态时, 我们使用 set() 方法, 假设我们在一个程序里, 我们需要存储 其他地方 的值添加到我们的状态, 为此, 我们将使用 Zustand 提供的方法 get() 代替, 此方法允许多个状态使用相同的值

js 复制代码
// 第二个参数 get
const useStore = create((set,get) => ({
  votes: 0,
  action: () => {
    // 使用 get()
    const userVotes = get().votes
    // ...
  }
}));

四,和 Redux 状态库对比

Redux 是一个非常流行的状态管理库,它提供了一种可预测的状态容器。然而,Redux 的一些缺点是其冗长的代码和引入许多概念,如 actions、reducers 和 middleware。这可能会让新手感到困惑,同时增加了应用程序的复杂性。

相比之下,Zustand 提供了一种更简洁的 API,无需引入额外的概念。它允许您直接使用 setState 更新状态,而无需编写繁琐的 actions 和 reducers。此外,Zustand 的体积更小,仅为 1KB,而 Redux 的体积为 7KB。

1,Redux

js 复制代码
import { createStore } from 'redux'
import { useSelector, useDispatch } from 'react-redux'

type State = {
  count: number
}

type Action = {
  type: 'increment' | 'decrement'
  qty: number
}

const countReducer = (state: State, action: Action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + action.qty }
    case 'decrement':
      return { count: state.count - action.qty }
    default:
      return state
  }
}

const countStore = createStore(countReducer)

const Component = () => {
  const count = useSelector((state) => state.count)
  const dispatch = useDispatch()
  // ...
}

2,zustand

js 复制代码
import { create } from 'zustand'

type State = {
  count: number
}

type Actions = {
  increment: (qty: number) => void
  decrement: (qty: number) => void
}

const useCountStore = create<State & Actions>((set) => ({
  count: 0,
  increment: (qty: number) => set((state) => ({ count: state.count + qty })),
  decrement: (qty: number) => set((state) => ({ count: state.count - qty })),
}))

const Component = () => {
  const { count , increment , decrement} = useCountStore();
  // ...
}

可以看出 zustand 使用起来非常简单,没有啥心智负担。

五,踩坑点

举个例子:

创建一个存放主题和语言类型的store

js 复制代码
import { create } from 'zustand';

interface State {
  theme: string;
  lang: string;
}

interface Action {
  setTheme: (theme: string) => void;
  setLang: (lang: string) => void;
}

const useConfigStore = create<State & Action>((set) => ({
  theme: 'light',
  lang: 'zh-CN',
  setLang: (lang: string) => set({lang}),
  setTheme: (theme: string) => set({theme}),
}));

export default useConfigStore;

分别创建两个组件,主题组件和语言类型组件

js 复制代码
import useConfigStore from './store';

const Theme = () => {

  const { theme, setTheme } = useConfigStore();
  console.log('theme render');
  
  return (
    <div>
      <div>{theme}</div>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>切换</button>
    </div>
  )
}

export default Theme;
js 复制代码
import useConfigStore from './store';

const Lang = () => {

  const { lang, setLang } = useConfigStore();

  console.log('lang render...');

  return (
    <div>
      <div>{lang}</div>
      <button onClick={() => setLang(lang === 'zh-CN' ? 'en-US' : 'zh-CN')}>切换</button>
    </div>
  )
}

export default Lang;

按照上面写法,改变theme会导致Lang组件渲染,改变lang会导致Theme重新渲染,但是实际上这两个都没有关系,怎么优化这个呢,有以下几种方法。

方案一:

js 复制代码
import useConfigStore from './store';

const Theme = () => {

  const theme = useConfigStore((state) => state.theme);
  const setTheme = useConfigStore((state) => state.setTheme);

  console.log('theme render');

  return (
    <div>
      <div>{theme}</div>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>切换</button>
    </div>
  )
}

export default Theme;

把值单个return出来,zustand内部会判断两次返回的值是否一样,如果一样就不重新渲染。

这里因为只改变了lang,theme和setTheme都没变,所以不会重新渲染。

方案二:

上面写法如果变量很多的情况下,要写很多遍useConfigStore,有点麻烦。可以把上面方案改写成这样,变量多的时候简单一些。

tsx 复制代码
import useConfigStore from './store';

const Theme = () => {

  const { theme, setTheme } = useConfigStore(state => ({
    theme: state.theme,
    setTheme: state.setTheme,
  }));

  console.log('theme render');

  return (
    <div>
      <div>{theme}</div>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>切换</button>
    </div>
  )
}

export default Theme;

上面这种写法是不行的,因为每次都返回了新的对象,即使theme和setTheme不变的情况下,也会返回新对象,zustand内部拿到返回值和上次比较,发现每次都是新的对象,然后重新渲染。

上面情况,zustand提供了解决方案,对外暴露了一个useShallow方法,可以浅比较两个对象是否一样。

tsx 复制代码
import { useShallow } from 'zustand/react/shallow';
import useConfigStore from './store';

const Theme = () => {

  const { theme, setTheme } = useConfigStore(
    useShallow(state => ({
      theme: state.theme,
      setTheme: state.setTheme,
    }))
  );

  console.log('theme render');

  return (
    <div>
      <div>{theme}</div>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>切换</button>
    </div>
  )
}

export default Theme;

六,数据持久化

你可以将 Zustand 的状态保存到 localStorage 或者 IndexedDB 中。当然,你需要注意的是,这种方式可能会导致一些问题,比如性能问题,以及在某些浏览器中可能会因为隐私设置而无法工作。

js 复制代码
// store.js
import create from 'zustand';
import { persist } from 'zustand-persist';

const initialState = {
  count: 0,
  increment: () => {},
  decrement: () => {},
};

const useStore = create(
  persist(
    (set) => ({
      ...initialState,
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
    }),
    {
      name: 'my-store', // 唯一名称
      getStorage: () => localStorage, // 可选,默认使用 localStorage
    }
  )
);

export default useStore;

在这个例子中,我们创建了一个简单的计数器应用的状态管理。incrementdecrement 函数分别用于增加和减少计数。我们使用 persist 函数将状态保存到 localStorage 中。

在你的 React 组件中使用这个 Zustand store:

js 复制代码
// App.js
import React from 'react';
import useStore from './store';

function App() {
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);
  const decrement = useStore((state) => state.decrement);

  return (
    <div>
      <h1>计数器: {count}</h1>
      <button onClick={increment}>增加</button>
      <button onClick={decrement}>减少</button>
    </div>
  );
}

export default App;

在这个组件中,我们使用 useStore 自定义 hook 来访问状态和操作函数。当用户点击"增加"或"减少"按钮时,计数器的值将会改变,并自动保存到 localStorage 中。

当应用重启时,zustand-persist 会自动从 localStorage 中加载状态,这样你就可以实现数据持久化了。

需要注意的是,如果你的状态中包含了不能直接保存到 localStorage 的数据(比如函数或者包含循环引用的对象),你需要在 persist 函数的配置对象中提供 serializedeserialize 函数来处理这些数据的序列化和反序列化。例如:

js 复制代码
const useStore = create(
  persist(
    (set) => ({
      ...initialState,
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
    }),
    {
      name: 'my-store',
      getStorage: () => localStorage,
      serialize: (state) => {
        // 处理序列化逻辑
      },
      deserialize: (serializedState) => {
        // 处理反序列化逻辑
      },
    }
  )
);

七,async operation 异步操作

如果你需要在 Zustand 的状态中处理异步操作,你可以在你的状态对象中添加一个异步函数。这个函数可以使用 set 函数来更新状态。

这里有一个例子,它展示了如何在 Zustand 状态中添加一个异步函数来从服务器加载数据:

js 复制代码
import create from 'zustand'

const useStore = create((set) => ({
  items: [],
  fetchItems: async () => {
    const response = await fetch('/api/items')
    const items = await response.json()
    set({ items })
  },
}))

在这个例子中,fetchItems 函数是一个异步函数,它使用 fetch API 从服务器加载数据,然后使用 set 函数更新 items 状态。

你可以在你的 React 组件中使用这个函数:

js 复制代码
import React, { useEffect } from 'react'
import useStore from './store'

function Items() {
  const items = useStore((state) => state.items)
  const fetchItems = useStore((state) => state.fetchItems)

  useEffect(() => {
    fetchItems()
  }, [fetchItems])

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}

export default Items

在这个组件中,我们使用 useEffect hook 在组件挂载时调用 fetchItems 函数。当 fetchItems 函数完成时,它会更新 items 状态,这将触发组件重新渲染。

注意,因为 fetchItems 是一个异步函数,所以你需要确保你的组件在等待数据加载时能正确处理。例如,你可能需要在数据加载时显示一个加载指示器,或者在数据加载失败时显示一个错误消息。

八,中间件

在 Zustand 中,你可以使用中间件来扩展或自定义状态管理的行为。中间件是一个函数,它接收一个 config 对象作为参数,并返回一个新的 config 对象。你可以在中间件中修改或增强状态更新的行为。

下面是一个简单的例子,展示了如何创建一个用于记录状态更新的日志的中间件:

js 复制代码
// loggerMiddleware.js
const loggerMiddleware = (config) => (set, get, api) => {
  const newSet = (partial, replace) => {
    console.log('更新前的状态:', get());
    console.log('应用的更新:', partial);
    set(partial, replace);
    console.log('更新后的状态:', get());
  };

  return {
    ...config(set, get, api),
    set: newSet,
  };
};

export default loggerMiddleware;

在这个例子中,我们创建了一个名为 loggerMiddleware 的中间件。这个中间件接收一个 config 对象,并返回一个新的 config 对象。我们在这个中间件中覆盖了 set 函数,以便在每次状态更新时输出日志。

要在你的 Zustand store 中使用这个中间件,你需要使用 create 函数的第二个参数传递它:

js 复制代码
// store.js
import create from 'zustand';
import loggerMiddleware from './loggerMiddleware';

const useStore = create(
  (set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
    decrement: () => set((state) => ({ count: state.count - 1 })),
  }),
  loggerMiddleware
);

export default useStore;

现在,每当你的状态发生变化时,loggerMiddleware 中间件将输出日志,显示更新前的状态、应用的更新以及更新后的状态。

你可以在 Zustand 中使用多个中间件。要实现这一点,只需将它们作为数组传递给 create 函数的第二个参数即可:

js 复制代码
import create from 'zustand';
import loggerMiddleware from './loggerMiddleware';
import anotherMiddleware from './anotherMiddleware';

const useStore = create(
  (set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
    decrement: () => set((state) => ({ count: state.count - 1 })),
  }),
  [loggerMiddleware, anotherMiddleware]
);

export default useStore;

在这个例子中,我们将 loggerMiddlewareanotherMiddleware 作为中间件数组传递给 create 函数。这些中间件将按照数组中的顺序应用。

Immer middleware

Immer 也可以作为中间件使用。

javascript 复制代码
import { create } from 'zustand-vue'

// import { create } from 'zustand'

import { immer } from 'zustand/middleware/immer'

const useBeeStore = create(
  immer((set) => ({
    bees: 0,
    addBees: (by) =>
      set((state) => {
        state.bees += by
      }),
  }))
)

Redux middleware

让你像写 redux 一样,来写 zustand

typescript 复制代码
import { redux } from 'zustand/middleware'

const types = { increase: 'INCREASE', decrease: 'DECREASE' }

const reducer = (state, { type, by = 1 }) => {
  switch (type) {
    case types.increase:
      return { grumpiness: state.grumpiness + by }
    case types.decrease:
      return { grumpiness: state.grumpiness - by }
  }
}

const initialState = {
  grumpiness: 0,
  dispatch: (args) => set((state) => reducer(state, args)),
}

const useReduxStore = create(redux(reducer, initialState))

Devtools middle

利用开发者工具 调试/追踪 Store

dart 复制代码
import { devtools, persist } from 'zustand/middleware'

const useFishStore = create(
  devtools(persist(
    (set, get) => ({
      fishes: 0,
      addAFish: () => set({ fishes: get().fishes + 1 }),
    }),
  ))
)
相关推荐
gqkmiss17 分钟前
Chrome 浏览器 131 版本开发者工具(DevTools)更新内容
前端·chrome·浏览器·chrome devtools
Summer不秃23 分钟前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰27 分钟前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
Viktor_Ye34 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm36 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
乐闻x1 小时前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
一条晒干的咸魚1 小时前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
Amd7941 小时前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子
生椰拿铁You1 小时前
09 —— Webpack搭建开发环境
前端·webpack·node.js
狸克先生2 小时前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互