The Lost Model
When we talk about React, we often repeat the mantra:
UI = f(State)
React does an excellent job solving the View layer.
But when it comes to the Model layer, the ecosystem has been searching for answers for years.
From Redux, to Hooks, to Zustand, we’ve been moving toward atomic, fragmented state management.
This brings minimal APIs and great ergonomics—but also a serious side effect:
The Model itself is broken apart.
Have you seen this pattern before?
-
State lives in a
create()function. -
Computed values are scattered across
useMemocalls or selector functions. -
Behavior (Actions) ends up inside
useEffects or event handlers.
The Model disappears, replaced by logic fragments spread everywhere.
Zenith: Rebuilding the Model Layer
Zenith focuses on high cohesion through co-location.
It brings State, Computed, and Actions back together—into a single, self-consistent unit.
Zenith = Zustand’s minimalism + MobX’s organization + Immer’s immutable foundation
Core Idea: An “Honest” Model
1. A Complete Model Definition (Co-location)
In Zenith, you don’t “peek” at state using get() inside closures, and you don’t rely on opaque set logic.
A Store is a complete business model.
class TodoStore extends ZenithStore<State> {
// 1. State
constructor() {
super({ todos: [], filter: 'all' });
}
// 2. Computed
// No selectors. No useMemo.
// Just native getters for derived state.
@memo((s) => [s.state.todos, s.state.filter])
get filteredTodos() {
const { todos, filter } = this.state;
// ...logic
}
// 3. Actions
// Use `this` honestly.
// The UI must never touch state directly.
addTodo(text: string) {
this.produce((draft) => {
draft.todos.push({ text, completed: false });
});
}
}
This is not about OOP for its own sake—it’s about restoring conceptual integrity.
2. Chained Derivations: Automatic Data Flow
One of MobX’s most attractive features is its automatic reactivity.
Zenith reproduces this behavior—but on top of immutable data.
You can derive one computed value from another:
A → B → C
When A changes, C updates automatically.
No dependency arrays.
No manual memo chains.
No derived logic leaking into components.
All computation stays inside the Model.
3. Components as Views — Zustand-Level Simplicity
Strict models are useless if consuming them is painful.
Zenith keeps the React side extremely simple, fully aligned with Hooks.
No HOCs.
No connect.
Just hooks.
const { useStore, useStoreApi } = createReactStore(TodoStore);
function TodoList() {
// ✅ Select state like Zustand
// Re-renders only when filteredTodos changes
const todos = useStore((s) => s.filteredTodos);
// ✅ Access the full Model instance (Actions)
const store = useStoreApi();
return (
<div>
{todos.map((todo) => (
// UI triggers intent, not business logic
<div onClick={() => store.toggle(todo.id)}>
{todo.text}
</div>
))}
</div>
);
}
The UI stays declarative.
The Model stays authoritative.
4. Engineering-First by Design
Zenith is not just a state library.
It ships with built-in History (undo / redo) and DevTools middleware, designed for complex applications.
I use it in domd—a Markdown WYSIWYG editor that smoothly handles 20,000+ lines of content.
This is not theoretical architecture.
It’s battle-tested.
Closing Thoughts
Zenith is not here to argue whether FP or OOP is “better”.
It simply acknowledges a reality:
When your application logic grows,
when you’re tired of jumping across dozens of Hooks and files to understand one feature—
you deserve a complete, honest Model layer.
Bring order back to your codebase.
GitHub: https://github.com/do-md/zenith
Stars ⭐ and issues are welcome.
Top comments (0)