Zustand:强大的 React 状态管理工具
一、Zustand基本介绍:
1.官方定义
基于 Flux
模型实现的小型、快速和可扩展的状态管理解决方案,拥有基于 hooks
的舒适的API,非常地灵活且有趣.
不要因为它很娇小就忽略它,它拥有锋利的爪子,花费了大量时间用来处理常见的陷阱,比如出现在多个复杂渲染器之间的 zombie child、react concurrency、以及 context loss 问题. Zustand
可以作为 React 应用中的一个状态管理器来正确处理这些问题。
- Zustand 在德语中是 state 状态的意思
2.基本使用
Step 1: 安装
npm install zustand # or yarn add zustand
Step 2: Store 初始化
创建的 store 是一个 hook
,你可以放任何东西到里面:基础变量,对象、函数,状态必须不可改变地更新,set
函数合并状态以实现状态更新。
import { create } from 'zustand'
const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
Step 3: Store 绑定组件,就完成了!
可以在任何地方使用钩子,不需要提供 provider
。
基于 selector
获取您的目标状态,组件将在状态更改时重新渲染。
##### 选择目标状态:bears
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <h1>{bears} around here ...</h1>
}
##### 更新目标状态:bears
function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
二、对比其他状态管理框架
为什么是 zustand 而不是 redux?
- 轻巧灵活
- 将
hooks
作为消费状态的主要手段 - 不需要使用
context provider
包裹你的应用程序 - 可以做到瞬时更新(不引起组件渲染完成更新过程)
通过列子对比两个状态库的区别:
Redux示例
import { createStore } from 'redux';
import { useSelector, useDispatch } from 'react-redux';
const initialState = {
count: 0,
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + action.qty };
case 'DECREMENT':
return { count: state.count - action.qty };
default:
return state;
}
};
const store = createStore(reducer);
const Component = () => {
const count = useSelector((state) => state.count);
const dispatch = useDispatch();
// 使用dispatch来更新状态
};
Zustand示例
import { create } from 'zustand';
const useCountStore = create((set) => ({
count: 0,
increment: (qty) => set((state) => ({ count: state.count + qty })),
decrement: (qty) => set((state) => ({ count: state.count - qty })),
}));
const Component = () => {
const { count, increment, decrement } = useCountStore();
// 直接调用increment和decrement来更新状态
};
为什么是 zustand 而不是 react Context?
- 不依赖
react
上下文,引用更加灵活 - 当状态发生变化时
重新渲染的组件更少
- 集中的、基于
action
的状态管理
通过列子对比两个状态库的区别:
react Context示例
import React, { createContext, useContext, useState } from 'react';
// 创建上下文
const CounterContext = createContext();
// 创建提供者组件
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
const increment = () => setCount((prevCount) => prevCount + 1);
const decrement = () => setCount((prevCount) => prevCount - 1);
const value = {
count,
increment,
decrement,
};
return <CounterContext.Provider value={value}>{children}</CounterContext.Provider>;
}
// 在组件中使用上下文
function CounterComponent() {
const { count, increment, decrement } = useContext(CounterContext);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
Zustand示例
import create from 'zustand';
// 创建状态存储
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
// 在组件中使用状态
function CounterComponent() {
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
const decrement = useCounterStore((state) => state.decrement);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
三、Zustand的性能优势
在选择React状态管理库时,我们常常会被各种库的特性和API所困惑。Zustand作为一款新兴的状态管理库,以其轻量、简单和灵活的特性脱颖而出,成为了许多React开发者的新宠。让我们来看看Zustand的几大优势是如何让React项目的状态管理变得更加高效和优雅的。
1、轻量级设计
Zustand的代码库非常小,gzip压缩后仅有1KB大小,对项目性能的影响几乎微乎其微。在如今这个对应用加载速度和性能要求越来越高的时代,选择一个轻量级的状态管理库尤为重要。Zustand恰好满足了这一需求,让你的项目保持轻量,同时也具备强大的状态管理能力。
2、简洁的API
Zustand提供了清晰而简洁的API,使得开发者可以迅速上手,轻松管理状态。这一点对于初学者或是希望简化项目复杂度的开发者来说尤其友好。Zustand的API设计遵循“少即是多”的原则,通过最少的学习就能达到快速开发的目的。
3、基于Hook的状态管理
Zustand利用了React的hook机制,通过创建自定义hook来访问和更新状态。这种方式与函数组件和hooks的编程模型无缝集成,使得状态管理自然而流畅。对于已经习惯了React hooks的开发者来说,使用Zustand进行状态管理将会感到非常自然和便捷。
4、易于集成
Zustand能够与其他React库(如Redux和MobX)无缝共存,这意味着你可以在不放弃现有库的情况下,逐渐过渡到Zustand。这为项目的状态管理提供了更多的灵活性和选择性。
5、完整的TypeScript支持
Zustand全面支持TypeScript,增强了项目的健壮性和类型安全。在当前软件开发趋势中,TypeScript的重要性日益凸显,Zustand的这一特性让它在众多状态管理库中更加突出。
6、灵活性与可扩展性
Zustand允许根据项目需求组织状态树,适应不同的项目结构。同时,Zustand引入了中间件的概念,通过插件来扩展其功能。无论是日志记录、持久化存储,还是异步操作,中间件都可以让状态管理变得更加灵活和可扩展。
总而言之,Zustand以其轻量、简洁、灵活
的特性,为React项目的状态管理提供了一个高效且优雅的解决方案。无论是对于追求性能的高级开发者,还是希望简化开发流程的新手,Zustand都是一个值得尝试的选择。
多种状态管理工具优劣势对比
框架 | 原理 | 优点 | 缺点 |
---|---|---|---|
hooks context | 基于 react hook,开发者可实现内/外部存储 | 1. 使用简单 2. 不需要引用第三方库,体积最小 3. 支持存储全局状态,但在复杂应用中不推荐 4. 不依赖 react 上下文,可在组件外调用(外部存储的条件下) | 1. context value发生变化时,所有用到这个context的组件都会被重新渲染,基于 content 维护的模块越多,影响范围越大。 2.依赖 Context Provider 包裹你的应用程序,修改 store 无法在应用最顶层(App.tsx 层级)触发渲染 3. 受ui框架约束(react) 4. 依赖hooks调用 |
react-redux | Flux思想,发布订阅模式,遵从函数式编程,外部存储 | 1. 不依赖 react 上下文,可在组件外调用 2. 支持存储全局状态 3. redux 本身是一种通用的状态解决方案 | 1. 心智模型需要一些时间来理解,特别是当你不熟悉函数式编程的时候 2. 依赖 Context Provider 包裹你的应用程序,修改 store 无法在应用最顶层(App.tsx 层级)触发渲染 3.受 ui 框架约束(react) |
mobx | 观察者模式 + 数据截止,外部存储 | 1. 使用简单,上手门槛低 2. 不依赖 react 上下文,可在组件外调用 3. 支持存储全局状态 4.通用的状态解决方案 | 1.可变状态模型,某些情况下可能影响调试 2. 除了体积相对较大之外,笔者目前未感觉到较为明显的缺点,3.99M |
zustand | Flux思想,观察者模式,外部存储 | 1. 轻量,使用简单,上手门槛低 2. 不依赖 react 上下文,可在组件外调用 3. 支持存储全局状态 4. 通用的状态解决方案 | 1.框架本身不支持 computed 属性,但可基于 middleware 机制通过少量代码间接实现 computed ,或基于第三方库 zustand-computed 实现 |
jotai | 基于 react hook,内部存储 | 1. 使用简单 2. 组件颗粒度较细的情况下,jotai性能更好 3.支持存储全局状态 | 1. 依赖 react 上下文, 无法组件外调用,相对而言, zustand 在 react 环境外及全局可以更好地工作 2.受ui框架约束(react) |
recoil | 进阶版 jotai,基于 react hook + provider context,内部存储 | 相对于 jotai而言,会更重一些,但思想基本不变,拥有一些 jotai 未支持的特性及 api,如: 1.监听 store 变化 2. 针对 atom 的操作拥有更多的 api,编程上拥有更多的可能性,更加有趣 | 拥有 jotai 所有的缺点,且相对于 jotai 而言: 1.使用 recoil 需要 < RecoilRoot > 包裹应用程序 2. 编写 selector 会复杂一些 |
valtio | 基于数据劫持,外部存储 | 1. 使用简单,类mobx(类vue)的编程体验 2.支持存储全局状态 3.不依赖 react 上下文,可在组件外调用 4. 通用的状态解决方案 | 1.可变状态模型,某些情况下可能影响调试 2.目前笔者没发现其它特别大的缺点,个人猜测之所以star相对zustand较少,是因为 valtio 的数据双向绑定思想与 react 存在冲突。 |
四、Zustand基本用法
1、获取所有内容
你可以获取整个 store,但请记住这样做会导致组件在每次状态变化时都重新渲染!
const state = useBearStore();
2、选择多个状态片段
默认情况下,Zustand 使用严格相等(old === new
)来检测变化,这对于原子状态选择是高效的。
const nuts = useBearStore((state) => state.nuts);
const honey = useBearStore((state) => state.honey);
第二种写法,对象形式
const {
nuts,
honey
} = useBearStore((state) => ({
nuts:state.nuts,
honey:state.honey,
}));
如果你想构造一个包含多个状态选择的单个对象,类似于 Redux 的 mapStateToProps
,你可以使用 useShallow
来防止当选择器输出根据浅相等不变时不必要的重新渲染。
import { create } from "zustand";
import { useShallow } from "zustand/react/shallow";
const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}));
const { nuts, honey } = useBearStore(
useShallow((state) => ({ nuts: state.nuts, honey: state.honey })),
);
3、覆盖状态
set
函数有第二个参数,默认为 false
。而不是合并,它将替换状态模型。小心不要抹去你依赖的部分,如操作。
import omit from "lodash-es/omit";
const useFishStore = create((set) => ({
salmon: 1,
tuna: 2,
deleteEverything: () => set({}, true), // 清除整个 store,包括操作
deleteTuna: () => set((state) => omit(state, ["tuna"]), true),
}));
4、异步操作
当你准备好时,只需调用 set
,zustand 不介意你的操作是异步的还是同步的。
const useFishStore = create((set) => ({
fishies: {},
fetch: async (pond) => {
const response = await fetch(pond);
set({ fishies: await response.json() });
},
}));
5、在操作中读取状态
set
允许函数更新 set(state => result)
,但你依然可以通过 get
在外部访问状态。
const useSoundStore = create((set, get) => ({
sound: "grunt",
action: () => {
const sound = get().sound;
// ...
},
}));
6、在组件外部读写状态和响应变化
有时你需要以非响应式的方式访问状态或对 store 进行操作。对于这些情况,结果钩子有附加到其原型的实用程序函数。
const useDogStore = create(() => ({ paw: true, snout: true, fur: true }));
const paw = useDogStore.getState().paw; // 获取非响应式最新状态
const unsub1 = useDogStore.subscribe(console.log); // 监听所有变化
useDogStore.setState({ paw: false }); // 更新状态,触发监听器
unsub1(); // 取消订阅监听器
7、使用带选择器的订阅
如果你需要使用选择器订阅,subscribeWithSelector
中间件会有所帮助。
import { subscribeWithSelector } from "zustand/middleware";
const useDogStore = create(
subscribeWithSelector(() => ({ paw: true, snout: true, fur: true })),
);
const unsub2 = useDogStore.subscribe((state) => state.paw, console.log);
8、在没有 React 的情况下使用 zustand
Zustand 核心可以导入并在没有 React 依赖的情况下使用。唯一的区别是 create
函数不返回一个钩子,而是 API 实用程序。
import { createStore } from 'zustand/vanilla';
const store = createStore((set) => ...);
const { getState, setState, subscribe, getInitialState } = store;
export default store;
9、持久中间件
你可以使用任何一种存储来持久化你 store 里的数据。(其他中间件在demo中有体现)
import create from "zustand";
import { persist } from "zustand/middleware";
export const useStore = create(
persist(
(set, get) => ({
fishes: 0,
addAFish: () => set({ fishes: get().fishes + 1 }),
}),
{
name: "food-storage", // 唯一键
getStorage: () => sessionStorage, // (可选)默认使用'localStorage'
}
)
);
10、Redux 开发者工具
import { devtools } from 'zustand/middleware';
const usePlainStore = create(devtools((set) => ...)); // 使用了 Redux 开发者工具的 store
五、zustand 的工作原理
zustand = 发布订阅 + react hooks:
- Vanilla 层是发布订阅模式的实现,提供了setState、subscribe 和 getState 方法,并且前两者提供了 selector 和 equalityFn 参数,以供在只有原生 JS 模式下的正常使用,但 React 的使用过程中基本只会用该模块的发布订阅功能。
- React 层是 Zustand 的核心,实现了 reselect 缓存和注册事件的 listener 的功能,并且通过 forceUpdate 对组件进行重渲染。
- 积极拥抱hooks,不需要使用context providers包裹应用,遵循大道至简的原则,上手简单;
// create 函数实现
// api 本质就是就是 createStore 的返回值,也就是 Vanilla 层的发布订阅器
const api: CustomStoreApi = typeof createState === 'function' ? createStore(createState) : createState
// 这里的 useIsomorphicLayoutEffect 是同构框架常用 API 套路,在前端环境是 useLayoutEffect,在 node 环境是 useEffect
useIsomorphicLayoutEffect(() => {
const listener = () => {
try {
// 拿到最新的 state 与上一次的 compare 函数
const nextState = api.getState()
const nextStateSlice = selectorRef.current(nextState)
// 判断前后 state 值是否发生了变化,如果变化调用 forceUpdate 进行一次强制刷新
if (!equalityFnRef.current(currentSliceRef.current as StateSlice, nextStateSlice)) {
stateRef.current = nextState
currentSliceRef.current = nextStateSlice
forceUpdate()
}
} catch (error) {
erroredRef.current = true
forceUpdate()
}
}
// 订阅 state 更新
const unsubscribe = api.subscribe(listener)
if (api.getState() !== stateBeforeSubscriptionRef.current) {
listener()
}
return unsubscribe
}, [])
我们首先从第 24 行 api.subscribe(listener)
开始,这里先创建了 listener 的订阅,这就使得任何的 setState 调用都会触发 listener 的执行,接着回到 listener 函数的内部,利用 api.getState()
拿到了最新 state,以及上一次的 compare 函数 equalityFnRef,然后执行比较函数后判断值前后是否发生了改变,如果改变则调用 forceUpdate 进行一次强制刷新。
这就是 zustand 渲染层的原理,简单而精巧,zustand 实现状态共享的方式本质是将状态保存在一个对象里
六、Zustand不足
1、写一个和 setState 一样的功能,需要 n 行代码。
思考如下场景,就是代码中原本的一个 [count, setCount] = useState(0)
。想要把 count 和 setCount 都共享到 store 中,需要写上如下代码:
// 定义全局的 count, setCount
const useStore = create((set, get) =>({
count: 0,
setCount: () =>
set((state) => ({
count: state.count + 1,
})),
}));
// 组件中使用
const App = () => {
const [count, setCount] = useStore((state) => [state.count, state.setCount]);
return (
/* some render */
)
}
2、状态获取不够简洁
在 create
函数中,状态的获取就使用 get()
就能拿到所有状态,但是使用起来就是不够简洁。开发者往往习惯通过以下的方式获取:
const useStore = create((set, get) => ({
count: 0,
reportCount: () => {
// 原本 zustand 可以通过如下方式取值
const count = get().count;
const { count } = get();
// 但是可能更习惯这样的方式
// 通过 string 获取
const count = get('count');
// 通过 selector 获取
const count = get((state) => state.count);
}
}))
3、devtools 中间件配合起来使用不适
zustand 的 set 有第二个参数,表示是否对全部状态进行覆盖,这一点使用下来笔者发现不仅没有使用场景,反而容易犯错误。
而为了配合 devtools 给 set 的行为加上名称,就需要 set(xxx, false, '名称'),可以说是非常不便了。
七、Zustand中间件
1.immer
immer
中间件提供了一种方便的方式来处理不可变状态的更新。在状态更新期间创建和管理不可变副本,以便更轻松地进行状态的修改。
import create from 'zustand';
const store = create(immer(/* initial state */));
store.setState((state) => {
state.count += 1;
state.user.name = 'John';
});
在上面的代码中,将初始状态作为参数传递给 immer
中间件。
在状态 store 中,可以直接修改状态对象,而不需要创建不可变副本或手动进行深层复制。通过调用 setState
方法并传递一个回调函数,可以直接对状态进行修改。并且还会自动批处理多个状态更新操作,以提高性能并减少不必要的重新渲染。
2.persist 缓存
把zustand里的数据持久化到localstorage或sessionStorage中,官方提供了中间件,用起来很简单,我想和大家分享的是,只持久化某个字段,而不是整个对象。
persist(() => initialFoodValue, {
name: "food store" , // 缓存名称
storage: createJSONStorage(() => localStorage), //缓存方式
partialize: (state) =>
Object.fromEntries(
Object.entries(state).filter(
([key]) => !["mouse"].includes(key) //缓存排除mouse属性
)
)
})
3.subscribeWithSelector 订阅
subscribeWithSelector
中间件提供了一种订阅状态的特定部分,并在该部分发生变化时执行相应操作的功能。
//创建
subscribeWithSelector(
persist(() => initialFoodValue, {
name: "food store" , // 缓存名称
storage: createJSONStorage(() => localStorage), //缓存方式
partialize: (state) =>
Object.fromEntries(
Object.entries(state).filter(
([key]) => !["mouse"].includes(key) //缓存排除mouse属性
)
)
})
),
//调用
useEffect(()=>{
const unsub = useFoodStore.subscribe(
(state)=> state.fish,
(fish)=>{
if(fish > 6){
setBgColor('salmon');
} else{
setBgColor('lavender');
}
},
{
equalityFn: shallow, //浅层比较
fireImmediately: true, // 订阅创建后立即执行一次
}
);
return unsub; //通过调用 `unsub` 函数,取消对状态变化的订阅
},[])
4. devtools调试工具
当store里数据变得复杂的时候,可以使用浏览器插件(首先安装一个插件 Redux DevTools)来查看store里的数据,不过需要使用devtools
中间件
devtools(
//监听
subscribeWithSelector(
// 缓存
persist(
(set, get) => ({
cats: {
bigCats: 0,
smallCats: 0,
},
increaseBigCats: () =>
set((state) => {
state.cats.bigCats++;
}, false,'setBigCats'), //函数名称
{
name: "cat store", // 缓存名称
}
)
),
{
enabled: true, // 启用插件
name: "cat store", //在开发者工具中显示的名称
}
)
默认操作名称都是anonymous
这个名字,如果我们想知道调用了哪个函数,可以给set
方法传第三个参数,这个表示方法名,比如上文中的setBigCats
。
结语:
总的来说,Zustand 提供了一种简单、轻量级且灵活的状态管理解决方案,适用于小型到中型的 React 应用程序。它通过简化状态管理的复杂性和提供高性能的状态更新机制,帮助开发者更好地组织和共享状态,并提供了中间件支持以增强功能。
Zustand-demo地址: github.com/li-0801/Zus…
参考社区:
Zustand 官方文档地址 :docs.pmnd.rs/zustand/get…
Zustand 中文社区:awesomedevin.github.io/zustand-vue…
状态管理工具优劣势分析:github.com/AwesomeDevi…
转载自:https://juejin.cn/post/7371423114381443072