DEV Community

张一凡
张一凡

Posted on

I Built a React State Management Library that Combines the Best of Zustand and MobX

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

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

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

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

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

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

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

Links

Give it a ⭐️ if you find it useful!

Top comments (0)