为什么选择 ZenBox?

ZenBox 与其他 React 状态管理库的对比

背景

React 生态下已经有很多状态管理解决方案了,但大多数往往:

  • 使用繁琐(如 Redux 的样板代码)
  • API 设计复杂,需要学习新的使用范式(如 Recoil)
  • 需要额外的心智负担(如 Valtio 的 proxy 里 this 的使用)
  • ……

要是能有一种状态管理库,用起来既像 Zustand 一样简单,又像 Vue 一样符合直觉,就好了。

Zustand x Vue

ZenBox 就是这样一个状态管理库:它结合了 Vue 和 Zustand 的优点,用起来既简单又符合直觉,非常爽!

如果你喜欢 Zustand 和 Vue,你一定会喜欢 ZenBox!

核心功能对比

自动类型推断

Zustand: 需要手动定义类型

import { create } from "zustand";

interface BearState {
  bears: number;
  increase: (by: number) => void;
}

const useBearStore = create<BearState>()((set) => ({
  bears: 0,
  increase: (by) => set((state) => ({ bears: state.bears + by })),
}));

✅ ZenBox: 可以从初始状态自动推导类型,无需手写 interface

const store = createStore({
  count: 0,
  name: "ZenBox",
  increment: () => {
    store.setState((state) => {
      state.count++;
    });
    return store.value.count;
  },
});

store.value.count; // number
store.value.name; // string

// 甚至可以自动推导出函数返回值类型
store.value.increment(); // () => number

跨 Store 计算值

Zustand: 不支持,需要预先组合 store 以实现共享选择器和按需渲染

interface BearSlice {
  bears: number;
}

interface FishSlice {
  fishes: number;
}

const createBearSlice = () => ({ bears: 0 });
const createFishSlice = () => ({ fishes: 0 });

const useBoundStore = create<BearSlice & FishSlice>()((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
}));

const total = useBoundStore((state) => state.bears + state.fishes);

✅ ZenBox: 支持跨 Store 计算值,自动追踪依赖,按需 re-render

const bearStore = createStore({ bears: 0 });
const fishStore = createStore({ fishes: 0 });

// 像 1 + 1 = 2 一样简单
const total = useComputed(() => {
  return bearStore.value.bears + fishStore.value.fishes;
});

状态访问模式

Zustand: 手动传递 get()set() 参数,不支持自动类型推导

import { create } from "zustand";

const useCountStore = create((set, get) => ({
  count: 0,
  increment: () => {
    set({
      ...get(),
      count: get().count + 1,
    });
  },
}));

useCountStore.getState().count;
useCountStore.getState().increment();

Jotai: 通过参数传递 getset 函数,可读性较差,增加心智负担

const count1 = atom(1);
const count2 = atom(2);
const count3 = atom(3);

const sum = atom((get) => get(count1) + get(count2) + get(count3));

// 函数式写法需要更多样板代码
const atoms = [count1, count2, count3, ...otherAtoms];
const sum = atom((get) => atoms.map(get).reduce((acc, count) => acc + count));

const decrementCountAtom = atom(
  (get) => get(countAtom),
  (get, set, _arg) => set(countAtom, get(countAtom) - 1)
);

Valtio: 在 Proxy 里使用 this 和 getter 需要非常小心,容易出错

const state = proxy({
  count: 1,
  get doubled() {
    return this.count * 2;
  },
  user: {
    name: "John",
  },
  greetings: {
    get greetingEn() {
      return "Hello " + this.user.name; // 错误 - `this` 指向 `state.greetings`
    },
  },
});

// 快照里的 getter 调用正常
const snap = snapshot(state);
console.log(snap.doubled); // 2

// 但状态变化不会更新快照
state.count = 10;
console.log(snap.doubled); // 仍然是 2

✅ ZenBox: 可以使用统一的 store.value 接口处理所有操作,心智负担最小

// 读取状态
const count = store.value.count;

// 更新状态
store.value = { count: count + 1 };

// 调用方法
store.value.increment();

// 计算值
const doubleCount = useComputed(() => store.value.count * 2);

Immer 集成

Zustand: 需要手动设置 Immer 中间件

import { create } from "zustand";
import { immer } from "zustand/middleware/immer";

type State = {
  count: number;
};

type Actions = {
  increment: (qty: number) => void;
  decrement: (qty: number) => void;
};

export const useCountStore = create<State & Actions>()(
  immer((set) => ({
    count: 0,
    increment: (qty: number) =>
      set((state) => {
        state.count += qty;
      }),
    decrement: (qty: number) =>
      set((state) => {
        state.count -= qty;
      }),
  }))
);

✅ ZenBox: 开箱即用,无需手动配置

const store = createStore({
  count: 0,
  increment: (qty: number) => {
    store.setState((state) => {
      state.count += qty;
    });
  },
});

按需渲染

Zustand: 需要手动使用 useShallow 进行浅比较,以减少重复渲染

import { create } from "zustand";
import { useShallow } from "zustand/react/shallow";

const useMeals = create(() => ({
  papaBear: "large porridge-pot",
  mamaBear: "middle-size porridge pot",
  littleBear: "A little, small, wee pot",
}));

export const BearNames = () => {
  const names = useMeals(useShallow((state) => Object.keys(state)));

  return <div>{names.join(", ")}</div>;
};

✅ ZenBox: 默认使用浅比较(和 React 的 useMemo 一样)

const names = useComputed(() => Object.keys(bearStore.value));

需要时也可选择深比较:

const names = useComputed(() => Object.keys(bearStore.value), { deep: true });

性能提示: 大多数情况下使用浅比较即可。深比较开销更大,仅在必要时使用。

特性对比

特性ZenBoxZustand
学习曲线✅ 最小化(Vue 友好)✅ 较低
Vue 风格响应式useComputed/useWatch❌ 需要手动处理
TypeScript 支持✅ 完整自动推断⚠️ 需要手动接口定义
状态访问✅ 统一的 store.value 接口❌ 手动 get()/set()
跨 Store 计算✅ 自动依赖跟踪⚠️ 需要预先组合
组件作用域✅ 内置 Provider 支持状态隔离❌ 默认全局状态
Immer 集成✅ 内置支持⚠️ 需要中间件
持久化❌ 无内置支持⚠️ 需要中间件
开发工具❌ 无内置支持⚠️ 需要中间件
打包大小< 3KB 压缩后(不含 Immer)< 1KB 压缩后