TL;DR: Found a new React state manager that combines Zustand's simplicity with Vue's developer experience. It's called ZenBox, it's tiny (~100 lines of core code), and it might solve some pain points you didn't know you had.
Context: My State Management Journey
Been doing React for 5+ years. Started with Redux (the pain), moved to MobX (the magic), tried Recoil (RIP), settled on Zustand (the balance). Recently worked on a Vue project and... damn, computed
and watch
are nice.
// Vue - so natural it hurts
const greeting = computed(() => `Hello ${user.value.name}!`);
watch(
() => user.value.name,
(newName) => {
console.log(`Welcome, ${newName}!`);
}
);
Came back to React wondering: why can't we have nice things?
What I Found: ZenBox
Someone built exactly what I was thinking about. It's like Zustand but with Vue-inspired APIs that actually make sense.
// ZenBox - Vue vibes, React power
import { createStore, useComputed, useWatch } from "zenbox";
// All types are inferred. No interfaces needed.
const userStore = createStore({
name: "Alice",
age: 25,
updateName: (name: string) => userStore.setState({ name }),
});
function UserProfile() {
// Vue-like computed - automatic dependency tracking
const greeting = useComputed(() => `Hello, ${userStore.value.name}!`);
// Vue-like watch - react to changes elegantly
useWatch(
() => userStore.value.name,
(newName) => console.log(`Name changed to: ${newName}`)
);
return <h1>{greeting}</h1>;
}
If you squint, this could be Vue code. But it's React, and it works exactly like you'd expect.
The Good Stuff
1. No TypeScript Boilerplate
Instead of this Zustand dance:
interface UserState {
name: string;
age: number;
updateName: (name: string) => void;
}
const useUserStore = create<UserState>()((set) => ({
name: "Alice",
age: 25,
updateName: (name) => set((state) => ({ ...state, name })),
}));
You write this:
const userStore = createStore({
name: "Alice",
age: 25,
updateName: (name: string) => userStore.setState({ name }),
});
// All types are inferred. No interfaces needed.
The difference? I write the logic once, not twice. No more maintaining separate interfaces and implementations.
2. Cross-Store Dependencies Actually Work
This is where Zustand gets messy. Want to combine multiple stores? Good luck with that slice pattern.
ZenBox just... works:
const user = createStore({ name: "Alice" });
const settings = createStore({ theme: "dark" });
const notifications = createStore({ unread: [] });
function Header() {
const { greeting, isDarkMode, unreadCount } = useComputed(() => ({
greeting: `Hello ${user.value.name}`,
isDarkMode: settings.value.theme === "dark",
unreadCount: notifications.value.unread.length,
}));
return (
<header className={isDarkMode ? "dark" : "light"}>
<h1>{greeting}</h1>
{unreadCount > 0 && <Badge count={unreadCount} />}
</header>
);
}
ZenBox automatically tracks all three stores and only triggers re-renders when the computed values actually change.
3. Immer Built-In, Zero Config
const store = createStore({
todos: [],
addTodo: (text) => {
store.setState((state) => {
state.todos.push({ id: Date.now(), text, done: false });
});
},
});
Direct mutations work out of the box. No middleware, no configuration, no headaches.
4. Performance by Default
ZenBox uses shallow comparison by default (like React's useMemo
), so you get optimal performance without thinking about it:
// Only re-renders when the keys actually change
const keys = useComputed(() => Object.keys(todos.value));
Real Example: Todo App
Here's a complete todo implementation to show how it all fits together:
import { createStore, useComputed, useWatch } from "zenbox";
const todoStore = createStore({
todos: [] as { id: number; text: string; done: boolean }[],
filter: "all" as "all" | "active" | "completed",
addTodo: (text) => {
todoStore.setState((s) => {
s.todos.push({ id: Date.now(), text, done: false });
});
},
toggleTodo: (id) => {
todoStore.setState((s) => {
const todo = s.todos.find((t) => t.id === id);
if (todo) todo.done = !todo.done;
});
},
});
function TodoApp() {
// Computed values - like Vue
const filteredTodos = useComputed(() => {
const { todos, filter } = todoStore.value;
return filter === "active" ? todos.filter((t) => !t.done) : todos;
});
const stats = useComputed(() => {
const todos = todoStore.value.todos;
return {
total: todos.length,
remaining: todos.filter((t) => !t.done).length,
};
});
// Watch for side effects - like Vue
useWatch(
() => todoStore.value.todos,
(todos) => localStorage.setItem("todos", JSON.stringify(todos))
);
const [input, setInput] = useState("");
return (
<div>
<h1>Todos ({stats.remaining} remaining)</h1>
<form
onSubmit={(e) => {
e.preventDefault();
if (input.trim()) {
todoStore.value.addTodo(input.trim());
setInput("");
}
}}
>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add todo..."
/>
</form>
{filteredTodos.map((todo) => (
<div key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => todoStore.value.toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
</div>
))}
</div>
);
}
The Trade-offs
What ZenBox excels at:
- Vue-like developer experience in React
- Less TypeScript boilerplate
- Cross-store dependencies just work
- Immer built-in (direct mutations)
- Lightweight (~100 lines of core code)
What it doesn't have (yet):
- Newer library (less battle-tested)
- Smaller ecosystem than Zustand
- No built-in persistence
- No DevTools integration
Should You Switch?
Switch if:
- You're tired of Zustand's TypeScript ceremony
- You want Vue-like computed/watch APIs
- You need cross-store dependencies
- You're starting a new project
Stick with Zustand if:
- You need battle-tested stability
- You rely heavily on middleware ecosystem
- Your team is already productive with current setup
My Recommendation
I genuinely think ZenBox is the future of React state management. It takes the best parts of Zustand and adds the developer experience improvements we've been wanting.
Is it perfect? No. Is it ready for production? I think so (I'm using it). Will it replace Zustand for everyone? Probably not, but it should be on your radar.
Try it in your next side project. See if it clicks for you like it did for me.
npm install zenbox
👉 Visit https://zenbox.del.wang to view the full documentation.
Final Thoughts
Three months ago, I was a happy Zustand user. Today, I'm a ZenBox advocate. Not because it's perfect, but because it solved real pain points I was experiencing.
The Vue-inspired APIs aren't just syntactic sugar - they represent a different way of thinking about state that feels more natural to me. Your mileage may vary, but I think it's worth exploring.
What’s your take on React state management? Have you tried ZenBox? Share your thoughts in the comments.
Top comments (0)