Skip to main content

优点

Typescript

类型支持,推导支持

type Store = {
count: number,
increase: () => void,
decrease: () => void,
};

const useStore =
create <
Store >
((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: Math.max(state.count - 1, 0) })),
}));

需要在 create 的时候传入 state 范型就可以很好地支持 TypeScript

interface BearState {
bears: number;
increase: (by: number) => void;
}
const useStore = create<BearState>()(
devtools(persist(set => ({ bears: 0, increase: by => set(state => ({ bears: state.bears + by })) })))
);

为什么不能直接推断出类型? https://github.com/pmndrs/zustand/blob/main/docs/typescript.md#basic-usage

随时随地获取和更新状态

实际上 useStore 本身虽然是仅能用于 react 组件中的 hook,但它带有两个方法(函数本身也是对象), getState 和 setState 是可以在任何地方执行的:

function getCount() {
return useStore.getState().count;
}

function increaseCount() {
useStore.getState().increase();
}
  • 想在 useEffect 中获取最新的 state,但不想加入到依赖项
  • 在非 react 程序中获取到 state
  1. 移除 effect , memo , callback 中的不相关依赖项(有时候只是为了不违背 Rules of Hooks 而加上的)
// 当 id 后(通常取自路径/查询参数),重新拉取 detail 数据
useEffect(() => {
useStore.getState().loadDetail(id);
}, [id]);

const handleSubmit = useCallback(async () => {
// 直接获取当前的 loading 状态做防重处理
if (useStore.getState().loading) {
return;
}
try {
// ...
} catch {
} finally {
useStore.setState({ loading: false });
}
// 而无需将 loading 作为依赖项之一
}, []);
  1. Zustand 也可以在不依赖 React 的情况下使用,某些普通函数执行时直接获取当前状态
function customReport() {
reportUserLoggedIn(useStore.getState().user);
}
import createStore from 'zustand/vanilla'
const store = createStore(() => ({ ... }))
const { getState, setState, subscribe, destroy } = store

监听状态变更

// 监听所有状态改变,触发回调
const unsubscribe = useStore.subscribe((state) => console.log(state));

// 基于 selector 监听部分状态(需要使用 subscribeWithSelector 中间件)
const unsubscribe = useStore.subscribe(
(state) => state.count,
(state) => console.log(state)
);

异步 action

Zustand 并不关心 action 是同步的还是异步的,只要在完成后调用 setState 就行。

const useStore = create((set) => ({
fishies: {},
fetch: async (pond) => {
const response = await fetch(pond);
set({ fishies: await response.json() });
},
}));
const useStore = create((set, get) => ({
loading: false,
hash: "",
data: {},
update: async () => {
// 设置一个变量防重
if (get().loading) return;
try {
set({ loading: true });
const hash = await getDataHash();
// 判断 data 是否已是最新
if (hash === get().hash) return;
const data = await getData();
set({ data });
} catch {
} finally {
set({ loading: false });
}
},
}));

个人觉得,相对于其他状态管理库,如 redux 中的 redux-thunk / redux-sage 等异步动作模型, modernjs(reduck) 中的副作用而言,这种风格的代码更容易理解,也更难出错,

扩展性

zustand 实现了一套中间件机制,可以在 create 时对 createState (也就是 create 的第一个也是唯一一个入参) 进行拦截和封装操作,熟悉 redux 的同学应该会发现这一点与 redux 十分类似 (实际上实现原理也类似) ,只不过在 redux 中,拦截和封装的对象是 reducer

可以看到中间件实际上是一个高阶函数,它接受 createState 函数作为唯一参数,返回一个 createState 函数,在它内部,将中间件所需要实现的附加逻辑应用到一个新的 setState 和 getState 函数上,然后返回入参的 createState 将新 setState 和 getState 作为入参的执行结果

// 虽然高阶函数看起来是比较晦涩,可以尝试理解一下,下面解释完实现原理后就很清晰了
const log = (createState) => (set, get, api) =>
// 重新调用createState(set,get,api)
createState(
(args) => {
console.log(" applying", args);
set(args);
console.log(" new state", get());
},
get,
api
);

const useStore = create(
log((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: Math.max(state.count - 1, 0) })),
}))
);

基于这个中间件特性,能对 zustand 进行各种个样的能力扩展,使得 zustand 拥有了良好的扩展性,更新关于中间件的介绍详见 https://github.com/pmndrs/zustand#middleware

// 中间件甚至可以用来辅助 typing ,比如官方提供的 combine middleware ,实现代码也相当精简
const useStore = create(
combine(
{
count: 0,
},
(set) => ({
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: Math.max(state.count - 1, 0) })),
})
)
);

devtools

zustand 官方提供了一个 devtools 中间件,使之能够利用 redux devtool

import { devtools } from "zustand/middleware";

const useStore = create(
devtools(
(set) => ({
todos: [],
addTodo: (text) =>
set(
(state) => ({
todos: state.todos.concat({ id: `${Date.now}`, text }),
}),
false, // replace?: boolean, 是否将 partialState 直接替代 state
"addTodo" // Action Type
),
}),
{
name: "TodoApp", // 实例名
}
)
);

计算属性

官方文档里并没有提及这种使用场景,不过可以通过以下两种方式来实现

hook

const useStore = create((set) => ({
todos: [],
}));

const useCompletedTodos = () => {
const todos = useStore((state) => state.todos);
return useMemo(() => todos.filter((todo) => todo.completed), []);
};

这种方法的缺点是,当 todos 状态改变后,这个 hook 每被一个组件使用一次,这个 memo 计算就会增加一次,如果这个计算性能开销较大或者使用的组件过多的话,就不太适用了

subscribeWithSelector

import { subscribeWithSelector } from "zustand/middleware";

const useStore = create(
subscribeWithSelector((set, get) => ({
todos: [],
remainingTodosCount: 0,
computeRemainingTodosCount: () => {
set(
{
remainingTodosCount: get().todos.filter((todo) => !todo.completed)
.length,
},
false,
"computeRemainingTodosCount"
);
},
}))
);

// 当 todos 状态变更后,自动调用 computeRemainingTodoCount 来更新 remainingTodosCount 状态
useStore.subscribe(
(state) => state.todos,
() => useStore.getState().computeRemainingTodoCount()
);

总结

总的来说,zustand 的 API 十分精简,上手成本很低,并且它的功能相对来说是比较完善的,应对大部分 react 状态管理应用场景应该是完全足够的,正如它的那句描述一样,Bear necessities for state management in React