DEV Community

张一凡
张一凡

Posted on

Why I Switched from MobX to easy-model (And Why You Might Too)

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;
      });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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>,
);
Enter fullscreen mode Exit fullscreen mode

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)