为什么选择 ZenBox?

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

动机

我已经写了数年 React 了,期间用过许多状态管理库:

比如 Redux、MobX、Zustand、Jotai、Valtio 等,每个都有自己的特点和使用场景,但总感觉差点什么。

后来我开始接触 Vue 生态,第一次使用 computed 的时候,我想:"为什么 React 不能像 Vue 一样简单?"

Zustand x Vue

于是便有了 ZenBox —— 既有 Zustand 的简洁,又有 Vue 的愉悦开发体验,让 React 状态管理更简单。

核心差异

Vue 般的开发体验

useComputeduseWatch 让你像写 Vue 一样写 React。

const userStore = createStore({
  name: "小明",
  age: 25,
});

// 计算属性
const greeting = useComputed(() => `你好 ${userStore.value.name}!`);

// 监听变化
useWatch(
  () => userStore.value.name,
  (newName) => console.log(`名字变了: ${newName}`)
);

自动类型推导

其他库:

interface UserState {
  name: string;
  age: number;
  posts: Post[];
  updateName: (name: string) => void;
  addPost: (post: Post) => void;
}

const useUserStore = create<UserState>()((set) => ({
  // ... 再实现一遍所有东西,同时在 2 个地方维护方法类型
}));

✅ ZenBox:

// 自动推导类型,无需手写类型接口
const userStore = createStore({
  name: "小明",
  age: 25,
  posts: ["你好,世界"],
});

跨 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 计算属性?直接引用就行:

const user = createStore({ name: "小明" });
const posts = createStore({ items: [] });
const settings = createStore({ theme: "dark" });

// ZenBox 自动追踪这三个 store
const dashboard = useComputed(() => ({
  greeting: `你好 ${user.value.name}`,
  postCount: posts.value.items.length,
  isDarkMode: settings.value.theme === "dark",
}));

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",
}));

// 不使用浅比较,组件会重新渲染
const SlowBearNames = () => {
  const names = useMeals((state) => Object.keys(state)); // 总是重新渲染
  return <div>{names.join(", ")}</div>;
};

const BearNames = () => {
  // 手动浅比较,避免重复渲染
  const names = useMeals(useShallow((state) => Object.keys(state)));

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

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

// 默认使用浅比较 - 仅在 keys 变化时重新渲染
const names = useComputed(() => Object.keys(bearStore.value));

// 需要时也可选择深比较
const deepNames = useComputed(() => Object.keys(bearStore.value), {
  deep: true,
});

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

状态访问模式

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

import { create } from "zustand";

const useCountStore = create((set, get) => ({
  count: 0,
  increment: () => {
    // 手动传递 get() 和 set() 参数
    set({
      ...get(),
      count: get().count + 1,
    });
  },
}));

// 外部访问需要 getState()
useCountStore.getState().count;
useCountStore.getState().increment();

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

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

// 每一个依赖都需要显式调用 get()
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; // 使用 this 会有心智负担
  },
  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);

特性对比

特性ZenBoxZustand
学习曲线✅ 像 Vue 一样简单✅ 较低
Vue HooksuseComputed/useWatch❌ 不支持
TypeScript✅ 自动推断类型⚠️ 手动定义类型
跨 Store 计算属性✅ 自动跟踪依赖❌ 不支持
状态访问✅ 统一的 store.value 接口⚠️ 显式 get()/set()
状态隔离✅ 内置 Provider❌ 默认全局状态
Immer 集成✅ 内置支持⚠️ 需要中间件
持久化❌ 无内置支持⚠️ 需要中间件
DevTools❌ 无内置支持⚠️ 需要中间件
打包大小< 3KB 压缩后(不含 Immer)< 1KB 压缩后