DEV Community

张一凡
张一凡

Posted on

Rethinking React State Management with easy-model

How I learned to stop worrying about Redux boilerplate and love class-based models

The Problem with Traditional State Management

Building React applications often means wrestling with state management. Redux is powerful but verbose. MobX has a learning curve. Zustand is lightweight but limited.

What if you could describe your business logic in plain TypeScript classes and have everything else just work?

Let me introduce you to easy-model — a React state management library that changed how I think about frontend architecture.

The Core Idea: Classes as Models

The mental shift is simple:

Fields are state, methods are business logic.

export class CounterModel {
  count = 0;

  increment() {
    this.count += 1;
  }

  decrement() {
    this.count -= 1;
  }
}
Enter fullscreen mode Exit fullscreen mode

No actions. No reducers. No dispatch. Just a class.

Quick Comparison

Redux Version

// 4 files, 60+ lines for a simple counter
// actions/counter.ts
// reducers/counter.ts
// store.ts
// Counter.tsx
Enter fullscreen mode Exit fullscreen mode

easy-model Version

// counter.ts - one file
export class CounterModel {
  count = 0;
  increment() { this.count += 1; }
}

// Counter.tsx
const counter = useModel(CounterModel, []);
<button onClick={counter.increment}>{counter.count}</button>
Enter fullscreen mode Exit fullscreen mode

Key Features

1. React Hooks Integration

function BlogPostList() {
  const blog = useModel(BlogModel, []);

  return (
    <div>
      <button onClick={blog.fetchPosts}>Load Posts</button>
      {blog.posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

2. Shared Instances with provide

export const appContext = provide(
  class AppContext {
    currentUser: User | null = null;
    notifications: Notification[] = [];

    setUser(user: User) {
      this.currentUser = user;
    }

    addNotification(message: string) {
      this.notifications.push({
        id: Date.now().toString(),
        message,
        timestamp: new Date(),
      });
    }
  },
)();

// In components
function Navbar() {
  const { currentUser } = useInstance(appContext);
  return <div>{currentUser?.name || 'Guest'}</div>;
}

function NotificationBell() {
  const { notifications } = useInstance(appContext);
  return <Badge count={notifications.length} />;
}
Enter fullscreen mode Exit fullscreen mode

3. Automatic Loading States

class BlogModel {
  posts: Post[] = [];

  @loader.load(true) // Participates in global loading
  @loader.once // Only load once
  async fetchPosts() {
    const res = await fetch("/api/posts");
    this.posts = await res.json();
  }
}
Enter fullscreen mode Exit fullscreen mode

No more manual setLoading(true) -> fetch -> setLoading(false) -> handleError patterns.

4. Dependency Injection with Zod

// types/http.ts
import { z } from "zod";

export const HttpSchema = z.object({
  get: z.function().args(z.string()),
  post: z.function().args(z.string(), z.unknown()),
});

// Inject into any model
class BlogModel {
  posts: Post[] = [];

  @inject(HttpSchema)
  private http?: HttpClient;

  @loader.load(true)
  @loader.once
  async fetchPosts() {
    this.posts = (await this.http?.get("/api/posts")) as Post[];
  }
}
Enter fullscreen mode Exit fullscreen mode

Configure once, use everywhere:

// main.tsx
import { CInjection, config, Container } from "@e7w/easy-model";

config(
  <Container>
    <CInjection schema={HttpSchema} ctor={MockHttp} />
  </Container>
);
Enter fullscreen mode Exit fullscreen mode

5. Environment Switching

// Development
config(
  <Container>
    <CInjection schema={HttpSchema} ctor={MockHttp} />
  </Container>
);

// Production - just swap the implementation
// <CInjection schema={HttpSchema} ctor={AxiosHttp} />
Enter fullscreen mode Exit fullscreen mode

Real-World Example: Task Management

// models/task.ts
export class TaskModel {
  tasks: Task[] = [];

  @inject(HttpSchema)
  private http?: HttpClient;

  @loader.load(true)
  async fetchTasks() {
    this.tasks = (await this.http?.get('/api/tasks')) as Task[];
  }

  @loader.load(true)
  async createTask(title: string) {
    const task = (await this.http?.post('/api/tasks', { title })) as Task;
    this.tasks.push(task);
    return task;
  }

  toggleTask(id: string) {
    const task = this.tasks.find(t => t.id === id);
    if (task) task.completed = !task.completed;
  }
}

// pages/tasks/index.tsx
function TasksPage() {
  const taskModel = useModel(TaskModel, []);

  return (
    <div>
      <button onClick={taskModel.fetchTasks}>Refresh</button>
      <TaskForm onSubmit={taskModel.createTask} />
      {taskModel.tasks.map(task => (
        <TaskItem key={task.id} task={task} />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Performance Benchmarks

In stress tests with 1000 simultaneous component updates:

Library Time
Zustand ~0.6ms
easy-model ~3.1ms
MobX ~16.9ms
Redux ~51.5ms

17x faster than Redux while providing more features.

When to Use easy-model

Great for:

  • Medium to large React applications
  • TypeScript projects
  • Enterprise applications needing DI
  • Projects migrating from Redux/MobX
  • Complex form applications

Not ideal for:

  • Very simple apps (useState is fine)
  • Stateless UI components
  • Projects avoiding TypeScript

Getting Started

npm install @e7w/easy-model
Enter fullscreen mode Exit fullscreen mode

Conclusion

easy-model represents a different philosophy: instead of adapting your code to fit a state management pattern, let the pattern adapt to how you naturally write code.

Give it a try — your future self might thank you.

GitHub: https://github.com/ZYF93/easy-model


Have you tried class-based state management? Share your experience in the comments!

Top comments (0)