跳到主要内容

Zustand

Zustand 是基于 Flux 思想, 也就是类似 redux 单向数据流模型来实现的 小型、快速和可扩展的状态管理, 它有基于 hooks 舒适的 API, 非常灵活便捷。

信息

Zustand 官方文档地址:Zustand

Zustand 中文网:Zustand

Flux 单向数据流

Flux 单向数据流

redux

看到上图是不是很眼熟, 就是 redux 的基本原理了

默认版本是 react 版本, vue 的版本是 - Zustand-Vue, 另外还有一个 Zustand-Pub 是为 Iframe、微前端、Module Fedetation、模块化、组件化 等业务场景, 提供 跨应用、跨框架 的 状态管理 及 状态共享 能力。

优缺点

优点

  1. 轻量, 使用简单, 上手门槛低
  2. 不依赖 react 上下文, 可在组件外调用
  3. 支持存储全局状态
  4. 通用的状态解决方案
  5. 支持多个 store
  6. 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;