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;
}
}
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
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>
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>
);
}
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} />;
}
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();
}
}
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[];
}
}
Configure once, use everywhere:
// main.tsx
import { CInjection, config, Container } from "@e7w/easy-model";
config(
<Container>
<CInjection schema={HttpSchema} ctor={MockHttp} />
</Container>
);
5. Environment Switching
// Development
config(
<Container>
<CInjection schema={HttpSchema} ctor={MockHttp} />
</Container>
);
// Production - just swap the implementation
// <CInjection schema={HttpSchema} ctor={AxiosHttp} />
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>
);
}
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
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)