重新理解 React 状态管理:用类的方式思考业务

15 阅读4分钟

不知道你们有没有这种感觉——每次新建一个 React 项目,光是搭状态管理架子就要花半天。

Redux 太重,MobX 太玄,Zustand 用着挺爽但总觉得缺了点什么……

直到我发现了 easy-model,用类的方式思考业务,状态管理突然就变得直观了。

先说痛点

我们先来回顾一下传统的状态管理写法:

Redux:一个计数器要写多少文件?

// actions/counter.ts
const INCREMENT = 'counter/increment';
export const increment = () => ({ type: INCREMENT });

// reducers/counter.ts
const counterSlice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: {
    increment: (state) => { state.count += 1; },
  },
});

// store.ts
export const store = configureStore({
  reducer: { counter: counterSlice.reducer },
});

// Counter.tsx
function Counter() {
  const count = useSelector(state => state.counter.count);
  const dispatch = useDispatch();
  return <button onClick={() => dispatch(increment())}>{count}</button>;
}

4 个文件,60+ 行代码,就为了一个计数器?

用类的思维重新理解

easy-model 的核心理念很简单:字段就是状态,方法就是业务逻辑

// counter.ts 一个文件搞定
export class CounterModel {
  count = 0;

  increment() {
    this.count += 1;
  }
}
// Counter.tsx
function Counter() {
  const counter = useModel(CounterModel, []);
  return <button onClick={counter.increment}>{counter.count}</button>;
}

你没看错,就这么简单。

为什么类的方式更好?

1. 代码组织更自然

业务逻辑和状态天然绑定在一起,不用在 action、reducer、selector 之间来回跳。

// 任务模型 - 所有任务相关的状态和行为都在这里
export class TaskModel {
  tasks: Task[] = [];
  currentFilter: "all" | "active" | "completed" = "all";

  get filteredTasks() {
    return this.tasks.filter((t) => {
      if (this.currentFilter === "active") return !t.completed;
      if (this.currentFilter === "completed") return t.completed;
      return true;
    });
  }

  addTask(title: string) {
    this.tasks.push({ id: Date.now().toString(), title, completed: false });
  }

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

2. 类型推导更完整

IDE 能准确知道 this.tasks 是什么类型,refactor 也不容易出错。

3. 继承和复用

// 基础模型 - 自动处理 loading 和 error
class BaseModel {
  loading = false;
  error: string | null = null;

  @loader.load()
  async safeCall<T>(fn: () => Promise<T>) {
    try {
      return await fn();
    } catch (e) {
      this.error = e instanceof Error ? e.message : "Unknown error";
    }
  }
}

// 文章模型继承基础模型
class ArticleModel extends BaseModel {
  articles: Article[] = [];

  async fetchArticles() {
    await this.safeCall(async () => {
      const res = await fetch("/api/articles");
      this.articles = await res.json();
    });
  }
}

核心 API 一览

useModel - 创建/获取共享实例

// 创建带初始值的实例
const article = useModel(ArticleModel, []);
// 组件卸载时自动清理生命周期

useInstance - 获取共享实例

class AppState {
  user: User | null = null;
  theme: 'light' | 'dark' = 'light';

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

  toggleTheme() {
    this.theme = this.theme === 'light' ? 'dark' : 'light';
  }
}
// 先定义一个共享实例
export const appState = provide(AppState)();

// 在组件中使用
function Header() {
  const { theme, toggleTheme } = useInstance(appState);
  return <button onClick={toggleTheme}>{theme} 模式</button>;
}

provide - 实例缓存

相同参数返回相同实例,不同参数返回不同实例。

// 购物车 - 按用户 ID 隔离
export const cartStore = provide(CartModel);

// 同一用户获取同一实例
const cart1 = cartStore("user-123");
const cart2 = cartStore("user-123");
// cart1 === cart2 ✓ 同一个购物车

// 不同用户是不同实例
const cartUserB = cartStore("user-456");
// cartUserB !== cart1 ✓

依赖注入:让服务管理更优雅

这是 easy-model 最让我惊喜的功能——在 React 里也能用依赖注入了

场景:统一的 HTTP 客户端

// 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()),
});

// models/article.ts
export class ArticleModel {
  articles: Article[] = [];

  @inject(HttpSchema)
  private http?: HttpClient;

  @loader.load(true)
  @loader.once
  async fetchArticles() {
    this.articles = (await this.http?.get("/api/articles")) as Article[];
  }
}
// main.tsx
import { CInjection, config, Container } from "@e7w/easy-model";
import { AxiosHttp } from "./http/axios";
import { DevHttp } from "./http/dev";

config(
  <Container>
    {/* 开发环境用 DevHttp */}
    <CInjection schema={HttpSchema} ctor={DevHttp} />

    {/* 生产环境换 AxiosHttp */}
    {/* <CInjection schema={HttpSchema} ctor={AxiosHttp} /> */}
  </Container>,
);

所有 @inject(HttpSchema) 的地方都会自动注入对应的实现。

开发环境用模拟数据,生产环境换真实接口,一行配置搞定。

加载状态:再也不用手动写 loading

class ArticleModel {
  articles: Article[] = [];

  @loader.load(true) // 参与全局 loading
  @loader.once // 只加载一次
  async fetchArticles() {
    const res = await fetch("/api/articles");
    this.articles = await res.json();
  }
}

一行装饰器替代 setLoading(true) -> fetch -> setLoading(false) -> handleError 的样板代码。

性能怎么样?

官方的 benchmark(1000 个组件批量更新):

方案耗时
Zustand~0.6ms
easy-model~3.1ms
MobX~16.9ms
Redux~51.5ms

easy-model 在功能完整度和性能之间取得了很好的平衡。

适合什么场景?

强烈推荐:

  • 中大型 React 项目
  • TypeScript 项目(类型推导很爽)
  • 需要依赖注入的企业级应用
  • 从 Redux/MobX 迁移

不太适合:

  • 很简单的小项目(useState 够用)
  • 不想用 TypeScript 的项目

怎么开始?

npm install @e7w/easy-model

然后把文档看一遍,基本上半天就能上手。


最后说点个人感受:用 easy-model 之后,我发现自己开始用"模型"的视角去思考业务,而不是纠结于"这个状态该放哪个 store"。代码的聚合度更高了,也更容易测试。

如果你也在寻找更优雅的状态管理方案,不妨试试看。

有问题可以在评论区聊~

GitHub: github.com/ZYF93/easy-…