Why I Switched from MobX to easy-model
MobX was my go-to for class-based state management. Until I found something better.
My MobX Journey
Started with MobX 4, migrated to MobX 6. Used decorators everywhere:
class OrderStore {
@observable orders: Order[] = [];
@observable loading = false;
@computed
get totalAmount() {
return this.orders.reduce((sum, o) => sum + o.amount, 0);
}
@action
async fetchOrders() {
this.loading = true;
try {
const res = await api.getOrders();
runInAction(() => {
this.orders = res.data;
});
} finally {
runInAction(() => {
this.loading = false;
});
}
}
}
Then TypeScript started complaining. Then decorators got deprecated. Then I had to rewrite everything with makeAutoObservable.
The MobX Pain Points
1. Type Inference Hell
// This should work but doesn't
const orders = store.orders.map((o) => o.items);
// TypeScript: "Object is of type 'unknown'"
class Store {
@observable map = new Map<string, Order>();
getOrder(id: string) {
return this.map.get(id); // Returns Order | undefined
}
}
// Later...
const order = store.getOrder("123");
order.amount; // Property 'amount' does not exist on type 'Order | undefined'
2. Hidden Dependencies
class Store {
@observable firstName = "John";
@observable lastName = "Doe";
@computed
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
@computed
get greeting() {
return `Hello, ${this.fullName}`;
}
}
// When firstName changes, both fullName and greeting recompute
// But it's not obvious from reading the code
3. No Built-in IoC
Want dependency injection? No native support. You end up passing dependencies manually or using service locators.
Then I Discovered easy-model
Same Class Model, Better DX
class OrderModel {
orders: Order[] = [];
loading = false;
get totalAmount() {
return this.orders.reduce((sum, o) => sum + o.amount, 0);
}
async fetchOrders() {
this.loading = true;
const res = await api.getOrders();
this.orders = res.data;
this.loading = false;
}
}
No decorators needed. Plain TypeScript classes work out of the box.
Explicit Watching
const store = useModel(OrderModel, []);
// Explicit dependency - clear from the code
watch(store, (keys, prev, next) => {
console.log(`${keys.join(".")} changed`, prev, next);
});
Built-in IoC
import { inject, CInjection, Container, config } from "easy-model";
import { object, string } from "zod";
const configSchema = object({
apiUrl: string(),
}).describe("API Config");
class ApiService {
@inject(configSchema)
config?: { apiUrl: string };
async getData() {
return fetch(`${this.config?.apiUrl}/data`);
}
}
config(
<Container>
<CInjection
schema={configSchema}
ctor={ApiService}
params={["https://api.example.com"]}
/>
</Container>,
);
Comparison
| Feature | easy-model | MobX |
|---|---|---|
| Class-based | ✅ | ✅ |
| No decorators needed | ✅ | ❌ |
| Explicit APIs | ✅ | ❌ |
| Built-in IoC | ✅ | ❌ |
| Deep watching | ✅ | ✅ |
| TypeScript support | ✅ | ⚠️ |
| History/Undo-Redo | ✅ | ❌ |
Performance
Same benchmark - 100K elements, 5 rounds batch update:
- easy-model: 3.1ms
- MobX: 16.9ms
easy-model is ~5x faster.
Verdict
easy-model gives me:
- What I loved about MobX (class models)
- What was missing from MobX (IoC, explicit APIs)
- Better TypeScript support
- Better performance
Now my go-to for new React projects.
GitHub: ZYF93/easy-model
Try it and let me know what you think! ⭐️
Top comments (0)