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

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

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

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

先说痛点

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

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

typescript 复制代码
// 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 的核心理念很简单:字段就是状态,方法就是业务逻辑

typescript 复制代码
// counter.ts 一个文件搞定
export class CounterModel {
  count = 0;

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

你没看错,就这么简单。

为什么类的方式更好?

1. 代码组织更自然

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

typescript 复制代码
// 任务模型 - 所有任务相关的状态和行为都在这里
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. 继承和复用

typescript 复制代码
// 基础模型 - 自动处理 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 - 创建/获取共享实例

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

useInstance - 获取共享实例

typescript 复制代码
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 - 实例缓存

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

typescript 复制代码
// 购物车 - 按用户 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 客户端

typescript 复制代码
// 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[];
  }
}
tsx 复制代码
// 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

typescript 复制代码
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 的项目

怎么开始?

bash 复制代码
npm install @e7w/easy-model

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


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

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

有问题可以在评论区聊~

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

相关推荐
mldong6 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排7 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60618 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角9 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!9 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰9 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi10 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen11 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马12 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒12 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端