I Built a React State Management Library that Combines the Best of Zustand and MobX
After years of struggling with Redux boilerplate and MobX's type inference issues, I built my own state management library that hits the sweet spot.
The Problem
I loved Zustand for its simplicity:
const useStore = create((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}));
But it lacked:
- Class-based organization
- Dependency injection
- Deep property watching
MobX had class models and deep watching, but:
- Type inference was painful
- Hidden dependency tracking made debugging hard
- Learning curve was steep
Introducing: easy-model
class CounterModel {
count = 0;
increment() {
this.count += 1;
}
decrement() {
this.count -= 1;
}
}
function Counter() {
const counter = useModel(CounterModel, []);
return (
<div>
<h1>{counter.count}</h1>
<button onClick={() => counter.increment()}>+</button>
<button onClick={() => counter.decrement()}>-</button>
</div>
);
}
That's it. No actions, no reducers, no selectors.
Key Features
1. Dependency Injection (IoC)
import { CInjection, Container, config, inject } from "easy-model";
import { object, string } from "zod";
const apiSchema = object({
baseUrl: string(),
}).describe("API Config");
class UserApi {
@inject(apiSchema)
config?: { baseUrl: string };
async fetchUsers() {
return fetch(`${this.config?.baseUrl}/users`);
}
}
config(
<Container>
<CInjection
schema={apiSchema}
ctor={UserApi}
params={["https://api.example.com"]}
/>
</Container>,
);
2. Deep Watching
class OrderModel {
order = {
items: [{ name: "Product", price: 100 }],
customer: { name: "John" },
};
}
watch(order, (keys, prev, next) => {
console.log(`Path: ${keys.join(".")}`);
console.log(`Changed: ${prev} -> ${next}`);
});
order.order.items[0].price = 150;
// Output: Path: order.items.0.price
// Changed: 100 -> 150
3. History (Undo/Redo)
const order = useModel(OrderModel, []);
const history = useModelHistory(order);
order.value = 1;
order.value = 2;
order.value = 3;
history.back(); // Undo to 2
history.forward(); // Redo to 3
history.reset(); // Back to initial
4. Loading States
class UserModel {
@loader.load(true)
async fetchUser(id: string) {
return api.getUser(id);
}
}
function UserPage() {
const { isGlobalLoading, isLoading } = useLoader();
const user = useModel(UserModel, [id]);
return (
<button disabled={isLoading(user.fetchUser)}>
{isLoading(user.fetchUser) ? "Loading..." : "Fetch"}
</button>
);
}
Performance Benchmark
Testing with 100,000 element array, 5 rounds of batch updates:
| Library | Time (ms) |
|---|---|
| Zustand | 0.6 |
| easy-model | 3.1 |
| MobX | 16.9 |
| Redux | 51.5 |
easy-model is 3x slower than Zustand, but offers:
- Class-based organization
- Built-in IoC/DI
- Deep watching
- History support
Trade-off worth making for most projects.
When to Use
- Yes: Domain-driven apps, need for DI, deep watching requirements
- No: Very simple local state, or already happy with current solution
Installation
pnpm add @e7w/easy-model
Links
- GitHub: ZYF93/easy-model
- npm: @e7w/easy-model
Give it a ⭐️ if you find it useful!
Top comments (0)