Why ZenBox?
How ZenBox compares to other React state management libraries
Motivation
I've been writing React for years. I've used Redux, MobX, Zustand, Jotai, Valtio - you name it. Each has its place, but none felt quite right.
Then I worked on a Vue project. The moment I wrote my first computed value, I thought: "Why can't React be this simple?"

So I built ZenBox - it brings Zustand's simplicity with Vue's joyful developer experience to React.
What's actually different
Vue-like development experience
const userStore = createStore({
  name: "Alice",
  age: 25,
});
// Computed values that just work
const greeting = useComputed(() => `Hello ${userStore.value.name}!`);
// Watch changes like Vue
useWatch(
  () => userStore.value.name,
  (newName) => console.log(`Name changed: ${newName}`)
);You don't write TypeScript interfaces
Other libraries:
interface UserState {
  name: string;
  age: number;
  posts: Post[];
  updateName: (name: string) => void;
  addPost: (post: Post) => void;
}
const useUserStore = create<UserState>()((set) => ({
  // ... implement everything again, and maintain types in 2 places
}));✅ ZenBox:
const userStore = createStore({
  name: "Alice",
  age: 25,
  updateName: (name) =>
    userStore.setState((s) => {
      s.name = name;
    }),
});
// That's it. All types are inferred.Cross-store dependencies just work
Zustand: You need to manually create a combined store with all the slices.
interface BearSlice {
  bears: number;
}
interface FishSlice {
  fishes: number;
}
const createBearSlice = () => ({ bears: 0 });
const createFishSlice = () => ({ fishes: 0 });
// Complex setup
const useBoundStore = create<BearSlice & FishSlice>()((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
}));
const total = useBoundStore((state) => state.bears + state.fishes);✅ ZenBox: Want to compute something from multiple stores? Just reference them:
const user = createStore({ name: "Alice" });
const posts = createStore({ items: [] });
const settings = createStore({ theme: "dark" });
// ZenBox automatically tracks all three stores
const dashboard = useComputed(() => ({
  greeting: `Hello ${user.value.name}`,
  postCount: posts.value.items.length,
  isDarkMode: settings.value.theme === "dark",
}));Immer integration
Zustand: Manual middleware setup with type gymnastics
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
// Separate type definitions required
type State = {
  count: number;
  nested: { items: string[] };
};
type Actions = {
  increment: (qty: number) => void;
  addItem: (item: string) => void;
};
// Complex middleware wrapping
export const useCountStore = create<State & Actions>()(
  immer((set) => ({
    count: 0,
    nested: { items: [] },
    increment: (qty: number) =>
      set((state) => {
        state.count += qty;
      }),
    addItem: (item: string) =>
      set((state) => {
        state.nested.items.push(item);
      }),
  }))
);✅ ZenBox: Immer built-in, zero configuration
const store = createStore({
  count: 0,
  nested: { items: [] as string[] },
  increment: (qty: number) => {
    store.setState((state) => {
      state.count += qty; // Direct mutation works
    });
  },
  addItem: (item: string) => {
    store.setState((state) => {
      state.nested.items.push(item); // Nested mutations work
    });
  },
});
// Types are automatically inferred from initial statePerformance optimization
Zustand: Manual shallow comparison with imports
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",
}));
// Without useShallow, component re-renders on every state change
const SlowBearNames = () => {
  const names = useMeals((state) => Object.keys(state)); // Re-renders always
  return <div>{names.join(", ")}</div>;
};
const BearNames = () => {
  // Manual shallow comparison required
  const names = useMeals(useShallow((state) => Object.keys(state)));
  return <div>{names.join(", ")}</div>;
};✅ ZenBox: Shallow comparison by default (same as React's useMemo)
// Default to shallow comparison - only re-renders when keys change
const names = useComputed(() => Object.keys(bearStore.value));
// Deep comparison when needed
const deepNames = useComputed(() => Object.keys(bearStore.value), {
  deep: true,
});Performance Note: ZenBox uses shallow comparison by default for optimal performance. Deep comparison is available when needed but should be used sparingly.
State access patterns
Zustand: Manual get() and set() parameter passing
import { create } from "zustand";
const useCountStore = create((set, get) => ({
  count: 0,
  increment: () => {
    // Verbose parameter passing
    set({
      ...get(),
      count: get().count + 1,
    });
  },
}));
// External access requires getState()
useCountStore.getState().count;
useCountStore.getState().increment();Jotai: Verbose get and set functions with limited readability
const count1 = atom(1);
const count2 = atom(2);
const count3 = atom(3);
// Every dependency needs explicit get() calls
const sum = atom((get) => get(count1) + get(count2) + get(count3));
// Complex operations become unreadable
const atoms = [count1, count2, count3, ...otherAtoms];
const sum = atom((get) => atoms.map(get).reduce((acc, count) => acc + count));
// Write atoms require dual function signatures
const decrementCountAtom = atom(
  (get) => get(countAtom),
  (get, set, _arg) => set(countAtom, get(countAtom) - 1)
);Valtio: Proxy patterns with this context complexity
const state = proxy({
  count: 1,
  get doubled() {
    return this.count * 2; // Mental overhead with `this`
  },
  user: {
    name: "John",
  },
  greetings: {
    get greetingEn() {
      return "Hello " + this.user.name; // WRONG - `this` points to `state.greetings`
    },
  },
});
// Snapshots create confusion
const snap = snapshot(state);
console.log(snap.doubled); // 2
// State changes don't update snapshots automatically
state.count = 10;
console.log(snap.doubled); // Still 2 - stale!✅ ZenBox: Unified store.value interface for everything
const store = createStore({
  count: 0,
  increment: () =>
    store.setState((s) => {
      s.count++;
    }),
});
// Read state - always current
const count = store.value.count;
// Update state - direct assignment
store.value = { count: count + 1 };
// Call actions - same interface
store.value.increment();
// Computed values - automatic dependency tracking
const doubleCount = useComputed(() => store.value.count * 2);Feature comparison
| Feature | ZenBox | Zustand | 
|---|---|---|
| Learning Curve | ✅ As easy as Vue | ✅ Low | 
| Vue-like Hooks | ✅ useComputed / useWatch | ❌ Not supported | 
| TypeScript | ✅ Auto-inference | ⚠️ Manual interfaces | 
| Cross-Store | ✅ Auto tracking | ❌ Not supported | 
| State Access | ✅ Unified store.value | ⚠️ Explicit get() / set() | 
| Scoping | ✅ Built-in Provider | ❌ Global by default | 
| Immer | ✅ Built-in | ⚠️ Middleware required | 
| Persistence | ❌ No built-in support | ⚠️ Middleware required | 
| DevTools | ❌ No built-in support | ⚠️ Middleware required | 
| Bundle Size | < 3KB gzipped (without Immer) | < 1KB gzipped |