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).
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
Feature | ZenBox | Zustand |
---|---|---|
Learning Curve | ✅ Minimal (Vue-friendly) | ✅ Low |
Vue-like Reactivity | ✅ useComputed /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 |