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?
Enter ZenBox: Vue Vibes in React
So I built it. ZenBox brings Vue's joyful developer experience to React state management, with all the power of Zustand under the hood.
// 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.
What Makes ZenBox Different
1. No More TypeScript Gymnastics
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-World Example
Here's a complete todo app to show you 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 Design Philosophy
I built ZenBox around three core principles:
1. Joyful Developer Experience
Code should feel natural, not ceremonial. If you know Vue, ZenBox should feel familiar. If you don't, it should feel intuitive.
2. Zero Configuration
No middleware setup, no complex configuration. Immer integration, TypeScript inference, and performance optimization work out of the box.
3. Unified Interface
Whether you're reading state, updating state, or calling methods, everything uses the same store.value
pattern. No mental overhead switching between different APIs.
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
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? Yes—I'm using it in my own projects with confidence. 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.
I didn't build ZenBox to replace every state manager. I built it because I believe React developers deserve the same joyful experience that Vue developers have.
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)