一、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 的优势
-
轻量级 :Zustand 的整个代码库非常小巧,gzip 压缩后仅有 1KB,对项目性能影响极小。
-
简洁的 API :Zustand 提供了简洁明了的 API,能够快速上手并使用它来管理项目状态。 基于钩子: Zustand 使用 React 的钩子机制作为状态管理的基础。它通过创建自定义 Hook 来提供对状态的访问和更新。这种方式与函数式组件和钩子的编程模型紧密配合,使得状态管理变得非常自然和无缝。
-
易于集成 :Zustand 可以轻松地与其他 React 库(如 Redux、MobX 等)共存,方便逐步迁移项目状态管理。
-
支持 TypeScript:Zustand 支持 TypeScript,让项目更具健壮性。
-
灵活性:Zustand 允许根据项目需求自由组织状态树,适应不同的项目结构。
-
可拓展性 : 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;
在这个例子中,我们创建了一个简单的计数器应用的状态管理。increment
和 decrement
函数分别用于增加和减少计数。我们使用 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
函数的配置对象中提供 serialize
和 deserialize
函数来处理这些数据的序列化和反序列化。例如:
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;
在这个例子中,我们将 loggerMiddleware
和 anotherMiddleware
作为中间件数组传递给 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 }),
}),
))
)