Zustand
Zustand
是基于 Flux
思想, 也就是类似 redux
单向数据流模型来实现的 小型、快速和可扩展的状态管理, 它有基于 hooks
舒适的 API, 非常灵活便捷。
Flux 单向数据流
看到上图是不是很眼熟, 就是 redux
的基本原理了
默认版本是 react 版本, vue 的版本是 - Zustand-Vue
, 另外还有一个 Zustand-Pub
是为 Iframe、微前端、Module Fedetation、模块化、组件化 等业务场景, 提供 跨应用、跨框架 的 状态管理 及 状态共享 能力。
优缺点
优点
- 轻量, 使用简单, 上手门槛低
- 不依赖
react
上下文, 可在组件外调用 - 支持存储全局状态
- 通用的状态解决方案
- 支持多个 store
- TS 支持较好
缺点
框架本身不支持 computed
属性, 但可基于 middleware
机制通过少量代码间接实现 computed
, 或基于第三方库 zustand-computed
实现
安装
npm install zustand
# or
yarn add zustand
# or
pnpm add zustand
使用
创建 store
初始化
import { create } from "zustand";
// create():存在三个参数, 第一个参数为函数, 第二个参数为布尔值
// 第一个参数:(set、get、api)=>{…}
// 第二个参数:true/false
// 若第二个参数不传或者传false时, 则调用修改状态的方法后得到的新状态将会和create方法原来的返回值进行融合;
// 若第二个参数传true时, 则调用修改状态的方法后得到的新状态将会直接覆盖create方法原来的返回值。
export const useCountStore = create((set) => ({
count: 0,
time: 1234,
setCount: (num: number) => set({ count: num }),
inc: () => set((state) => ({ count: state.count + 1 })),
minus: () => set((state) => ({ count: state.count - 1 })),
}));
// 计数器 Demo 快速上手
import React from "react";
import { useCountStore } from "./store/count";
export default function Demo() {
// 在这里引入所需状态
const { count, setCount, inc, minus } = useCountStore();
return (
<div>
{count}
<input
onChange={(event) => {
setCount(Number(event.target.value));
}}
/>
<button onClick={inc}>增加</button>
<button onClick={minus}>减少</button>
</div>
);
}
Selector
const state = useCountStore();
上面的例子中, 在使用状态时, 只要每次状态更新, 组件都会重新进行更新, 这就导致, 如果状态很多的话, 更新太过频繁。
如何动态更新, 而不是每次状态改变就更新呢?
上面的方法同样可以这么写:
// 获取状态
const count = useCountStore((state) => state.count);
// 获取放发, 然后使用
const setCount = useCountStore((state) => state.setCount);
return (
<div>
{count}
<input
onChange={(event) => {
setCount(Number(event.target.value));
}}
/>
</div>
);
那么如果一次性获取多个状态的场景, 应该怎么处理?
import { shallow } from "zustand/shallow";
// 选择 Object, 当`state.count`或`state.time`发生变化后, 组件重新渲染
const { count, time } = useCountStore((state) => ({ count: state.count, time: state.time }), shallow);
// 选择 Array, 当`state.count`或`state.time`发现变化后, 组件重新渲染
const [count, time] = useCountStore((state) => [state.count, state.time], shallow);
// 选择 Map, 当`state.treats`的排序、数量和 key 发生变化后, 组件重新渲染
const treats = useCountStore((state) => Object.keys(state.treats), shallow);
当然也可以自己根据使用场景需求, 来重写对比函数
const treats = useBearStore(
(state) => state.treats,
(oldTreats, newTreats) => compare(oldTreats, newTreats)
);
获取状态/设置状态
// 获取
const useFishStore = create((set, get) => ({
state1: {},
getState: () => {
return get().state1;
},
}));
// 设置
const useFishStore = create((set) => ({
state1: {},
getState: (newState) => {
set((state) => {
return { ...state, state1: newState };
});
},
}));
层级较深的状态修改
// 常规操作
normalInc: () =>
set((state) => ({
deep: {
...state.deep,
nested: {
...state.deep.nested,
obj: {
...state.deep.nested.obj,
count: state.deep.nested.obj.count + 1,
},
},
},
}));
其他方式, 官方推荐了三个, 这里不多赘述, 根本就是针对深层数据的修改, 可以根据个人喜好使用
外部访问 订阅
有时我们需要以非响应式的方式访问状态, 或者对存储进行操作。对于这些情况, 生成的 hook 具有附加在其原型上的实用函数。
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()
// 当然, 你 可以像往常一样使用钩子
const Component = () => {
const paw = useDogStore((state) => state.paw)
...
订阅 selector
监听变化
你可以用到 subscribeWithSelector 中间件。
使用这个中间件, subscribe 新增了一些额外的功能。
// subscribe(selector, callback, options?: { equalityFn, fireImmediately }): Unsubscribe
import { subscribeWithSelector } from "zustand/middleware";
const useDogStore = create(subscribeWithSelector(() => ({ paw: true, snout: true, fur: true })));
// 仅限 `paw` 修改时, 触发监听
const unsub2 = useDogStore.subscribe((state) => state.paw, console.log);
// subscribe 还可以监听到旧值
const unsub3 = useDogStore.subscribe(
(state) => state.paw,
(paw, previousPaw) => console.log(paw, previousPaw)
);
// subscribe 也支持自定义相等函数
const unsub4 = useDogStore.subscribe((state) => [state.paw, state.fur], console.log, { equalityFn: shallow });
// 立即订阅并触发
const unsub5 = useDogStore.subscribe((state) => state.paw, console.log, {
fireImmediately: true,
});
异步
异步获取数据, 修改状态, 可以直接使用 async/await
const useFishStore = create((set) => ({
fishies: {},
fetch: async (pond) => {
const response = await fetch(pond);
set({ fishies: await response.json() });
},
}));
瞬时更新
瞬时更新经常发生在状态变化时
subscribe 函数允许组件绑定到状态部分, 而不必强制在更改时重新呈现。最好与 useEffect 结合使用, 以便在卸载时自动取消订阅。当允许您直接更改视图时, 这种方式将极大地优化你的应用性能。
const useScratchStore = create(set => ({ scratches: 0, ... }))
const Component = () => {
// 获取初始状态
const scratchRef = useRef(useScratchStore.getState().scratches)
// 挂载时连接到 store , 卸载时断开连接, 捕获引用中的状态变化
useEffect(() => useScratchStore.subscribe(
state => (scratchRef.current = state.scratches)
), [])
...
使用 Reducers 及 ActionTypes
需要像 Redux 一样的 Reducers 以及 ActionTypes?
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 useGrumpyStore = create((set) => ({
grumpiness: 0,
dispatch: (args) => set((state) => reducer(state, args)),
}));
const dispatch = useGrumpyStore((state) => state.dispatch);
dispatch({ type: types.increase, by: 2 });
或者, 使用中间件 redux-middleware 。它连接你的 reducer, 设置初始状态, 并向 state 本身 和 Store API 添加 dispatch 函数` 。
import { redux } from "zustand/middleware";
const useReduxStore = create(redux(reducer, initialState));
TS
它有非常完备的 TS 支持, 我们只需要定义最基 础的 TS 类型。
import { create } from "zustand-vue";
// import { create } from 'zustand'
// if you need middleware
// import { devtools, persist } from 'zustand/middleware'
interface BearStateType {
bears: number;
increase: (by: number) => void;
...
}
const useBearStore = create<BearStateType>()(
(set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
...
})
// if you need middleware
// devtools(
// persist(
// (set) => ({
// bears: 0,
// increase: (by) => set((state) => ({ bears: state.bears + by })),
// }),
// {
// name: 'bear-storage',
// }
// )
// )
);
中间件
对于状态管理的中间件, 其实本质就是高阶函数, 输入函数, 输出函数, 中间做一些针对性的事情(记录, 存储, 数据清洗, 数据处理等等)
数据持久化 persist
你可以基于你能想到的任何方式(localStorage/cookie/数据库等)将 store 中的 state 进行持久化存储。
当然, 你需要注意的是, 这种方式可能会导致一些问题, 比如性能问题, 以及在某些浏览器中可能会因为隐私设置而无法工作。
import { create } from "zustand";
// import { create } from "zustand-vue";
import { persist, createJSONStorage } from "zustand/middleware";
const useFoodStore = create(
persist(
(set, get) => ({
fishes: 0,
addAFish: () => set({ fishes: get().fishes + 1 }),
}),
{
name: "food-storage", // unique name
storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
}
)
);
开发者工具 devtools
都有了状态管理, 那肯定离不开开发调试工具, 这个中间件可以 利用开发者工具 调试/追踪 Store
import { devtools, persist } from "zustand/middleware";
const useFishStore = create(
devtools(
persist((set, get) => ({
fishes: 0,
addAFish: () => set({ fishes: get().fishes + 1 }),
}))
)
);
自定义中间件
在 Zustand 中, 你可以使用中间件来扩展或自定义状态管理的行为。中间件是一个函数, 它接收一个 config 对象作为参数, 并返回一个新的 config 对象。你可以在中间件中修改或增强状态更新的行为。
import { produce } from "immer";
import { create } from "zustand";
// 自定义中间件
// 日志中间件
const log = (config) => (set, get, api) =>
config(
(args) => {
console.log(" applying", args);
set(args);
console.log(" new state", get());
},
get,
api
);
// 将 set 方法变成一个 immer proxy
const immer = (config) => (set, get, api) =>
config(
(partial, replace) => {
const nextState = typeof partial === "function" ? produce(partial) : partial;
return set(nextState, replace);
},
get,
api
);
const middleWareText = create(
log(
immer((set) => ({
count: 0,
setCount: (num) => set({ count: num }),
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
)
);
export default middleWareText;
中间件集合
如果使用中间件数量多了, 随之而来的问题就是不利于阅读, 每个中间件包裹一层, 导致层级太深, 阅读起来会比较难, 这时候就需要这个中间件, 聚合所有中间件为一个。
这里不得不提到 ramda
这个 js 库, 典型的函数式编程的思路, 归纳总结了很多常用的 js 方法, 强烈推荐
地址 -> 在这里!
import create from "zustand";
import produce from "immer";
import pipe from "ramda/es/pipe";
/* 通过pipe集合任意数量的中间件 */
const createStore = pipe(log, immer, create);
const useStore = createStore((set) => ({
bears: 1,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
}));
export default useStore;