Why ZenBox?

How ZenBox compares to other React state management libraries

The Problem

The React ecosystem offers many state management solutions, but they often fall into two categories: overly complex (like Redux with its boilerplate) or cognitively demanding (like Valtio's proxy patterns with this bindings).

Zustand x Vue

ZenBox bridges this gap by combining Zustand's simplicity with Vue's reactive patterns, creating an API that feels both natural and powerful.

If you love Zustand & Vue, you'll love ZenBox!

ZenBox vs The Rest

Automatic Type Inference

Zustand: Manual type definitions required

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: Types flow naturally from your initial state.

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

store.value.count; // number (inferred)
store.value.name; // string (inferred)
store.value.increment(); // () => number (inferred)

Cross-Store Computed Values

Zustand: Pre-combine stores for shared selectors

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: Effortless computation with automatic dependency tracking

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

// Simple as 1 + 1 = 2
const total = useComputed(() => {
  return bearStore.value.bears + fishStore.value.fishes;
});

State Access Patterns

Zustand: Manual get() and set() parameter passing

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: Verbose get and set functions with limited readability

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

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

// Functional patterns require more boilerplate
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 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`
    },
  },
});

// Getter calls on snapshots work as expected
const snap = snapshot(state);
console.log(snap.doubled); // 2

// But state changes don't update snapshots
state.count = 10;
console.log(snap.doubled); // Still 2

✅ ZenBox: Unified store.value interface for everything

// Read state
const count = store.value.count;

// Update state
store.value = { count: count + 1 };

// Call actions
store.value.increment();

// Computed values
const doubleCount = useComputed(() => store.value.count * 2);

Immer Integration

Zustand: Manual middleware setup

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: Immer built-in, zero configuration

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

Optimized Re-renders

Zustand: Manual shallow comparison with 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: Intelligent diffing by default

// Shallow diffing by default
const names = useComputed(() => Object.keys(bearStore.value));

// Deep diffing when you need it
const names = useComputed(() => Object.keys(bearStore.value), { deep: true });

Performance Note: Use shallow comparison for most cases. Deep comparison is more expensive and should only be used when necessary.

Feature Comparison

FeatureZenBoxZustand
Learning Curve✅ Minimal (Vue-friendly)✅ Low
Vue-like ReactivityuseComputed/useWatch❌ Manual handling
TypeScript Support✅ Complete auto-inference⚠️ Manual interface definition
State Access✅ Unified store.value interface❌ Manual get()/set()
Cross-Store Computed✅ Automatic dependency tracking⚠️ Requires pre-combination
Store Scoping✅ Built-in Provider for isolation❌ Global by default
Immer Integration✅ Built-in support⚠️ Middleware required
Persistence❌ No built-in support⚠️ Middleware required
DevTools❌ No built-in support⚠️ Middleware required
Bundle Size< 3KB gzipped (without Immer)< 1KB gzipped