为什么选择 ZenBox?
ZenBox 与其他 React 状态管理库的对比
动机
我已经写了数年 React 了,期间用过许多状态管理库:
比如 Redux、MobX、Zustand、Jotai、Valtio 等,每个都有自己的特点和使用场景,但总感觉差点什么。
后来我开始接触 Vue 生态,第一次使用 computed 的时候,我想:"为什么 React 不能像 Vue 一样简单?"

于是便有了 ZenBox —— 既有 Zustand 的简洁,又有 Vue 的愉悦开发体验,让 React 状态管理更简单。
核心差异
Vue 般的开发体验
useComputed 和 useWatch 让你像写 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: 通过参数传递 get 和 set 函数,可读性较差,增加心智负担
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);特性对比
| 特性 | ZenBox | Zustand | 
|---|---|---|
| 学习曲线 | ✅ 像 Vue 一样简单 | ✅ 较低 | 
| Vue Hooks | ✅ useComputed/useWatch | ❌ 不支持 | 
| TypeScript | ✅ 自动推断类型 | ⚠️ 手动定义类型 | 
| 跨 Store 计算属性 | ✅ 自动跟踪依赖 | ❌ 不支持 | 
| 状态访问 | ✅ 统一的 store.value 接口 | ⚠️ 显式 get()/set() | 
| 状态隔离 | ✅ 内置 Provider | ❌ 默认全局状态 | 
| Immer 集成 | ✅ 内置支持 | ⚠️ 需要中间件 | 
| 持久化 | ❌ 无内置支持 | ⚠️ 需要中间件 | 
| DevTools | ❌ 无内置支持 | ⚠️ 需要中间件 | 
| 打包大小 | < 3KB 压缩后(不含 Immer) | < 1KB 压缩后 |