阅读视图

发现新文章,点击刷新页面。

做了一个AI聊天应用后,我决定试试这个状态管理库

AI应用前端最大的坑,不是LLM调用,而是状态管理

背景

最近做了个AI聊天应用,类似ChatGPT的那种。

本来想用Redux,毕竟老牌方案,结果被毒打了一遍。

Redux的痛

痛点1:状态类型爆炸

// AI聊天应用需要管理的状态
interface ChatState {
  // 对话
  messages: Message[];

  // 流式响应
  streamingText: string;
  isStreaming: boolean;

  // 工具调用
  toolCalls: ToolCall[];
  currentToolCall: ToolCall | null;
  toolResults: Record<string, any>;

  // 上下文
  contextWindow: Message[];
  longTermMemory: MemoryItem[];

  // 用户意图
  currentIntent: Intent | null;
  intentHistory: Intent[];

  // 执行状态
  currentStep: number;
  stepResults: Record<number, any>;

  // 错误处理
  errors: Error[];
  retryQueue: RetryItem[];

  // ...
}

这还只是一个聊天模块的状态。

痛点2:action type能写死你

// 光是状态更新action就能写30个
const ADD_MESSAGE = "chat/ADD_MESSAGE";
const UPDATE_STREAMING = "chat/UPDATE_STREAMING";
const APPEND_STREAMING = "chat/APPEND_STREAMING";
const START_TOOL_CALL = "chat/START_TOOL_CALL";
const COMPLETE_TOOL_CALL = "chat/COMPLETE_TOOL_CALL";
const UPDATE_CONTEXT = "chat/UPDATE_CONTEXT";
const ADD_INTENT = "chat/ADD_INTENT";
// ... 还有几十个

痛点3:跨组件同步难

// Chat组件
const messages = useSelector((s) => s.chat.messages);

// Status组件
const toolCalls = useSelector((s) => s.chat.toolCalls);

// 怎么保证两个组件状态一致?靠redux-thunk?middleware?

然后我用了easy-model

// 一个类搞定AI聊天全状态
class AIChatModel {
  // 对话
  messages: Message[] = [];

  // 流式响应
  streamingText = "";
  isStreaming = false;

  // 工具调用
  toolCalls: ToolCall[] = [];
  currentToolCall: ToolCall | null = null;
  toolResults: Map<string, any> = new Map();

  // 上下文
  contextWindow: Message[] = [];
  longTermMemory: MemoryItem[] = [];

  // 用户意图
  currentIntent: Intent | null = null;
  intentHistory: Intent[] = [];

  // 执行
  currentStep = 0;
  stepResults: Map<number, any> = new Map();

  // 错误
  errors: Error[] = [];

  // 方法
  @loader.load()
  async sendMessage(content: string) {
    this.messages.push({ role: "user", content });
    this.isStreaming = true;

    const response = await llm.streamChat(this.messages);

    for await (const chunk of response) {
      this.streamingText += chunk;
    }

    this.messages.push({ role: "assistant", content: this.streamingText });
    this.streamingText = "";
    this.isStreaming = false;
  }

  async executeToolCall(tool: Tool, params: any) {
    this.currentToolCall = { tool, params, status: "running" };
    this.toolCalls.push(this.currentToolCall);

    const result = await tool.execute(params);

    this.currentToolCall.status = "completed";
    this.currentToolCall.result = result;
    this.toolResults.set(tool.name, result);
    this.currentToolCall = null;
  }
}

一个类,200行代码搞定Redux 500行都搞不定的事。

还能解决什么问题?

1. 撤销重做,调试AI回复

const chat = useModel(AIChatModel, []);
const history = useModelHistory(chat);

// 用户想撤回AI的上一次回复?
history.back();

// 想重做?
history.forward();

2. 跨组件状态共享

// 聊天区域
function ChatArea() {
  const chat = useModel(AIChatModel, ["main"]);
  return <MessageList messages={chat.messages} />;
}

// 状态显示
function StatusPanel() {
  const chat = useModel(AIChatModel, ["main"]);
  return <StatusBadge isStreaming={chat.isStreaming} />;
}

// 工具调用面板
function ToolPanel() {
  const chat = useModel(AIChatModel, ["main"]);
  return <ToolList calls={chat.toolCalls} />;
}

// 三个组件,状态自动同步

3. 深度监听

// 监听任意状态变化
watch(chat, (keys, prev, next) => {
  console.log(`状态变化: ${keys.join(".")}`, prev, "→", next);

  // "messages.5.content" - 第6条消息内容变了
  // "toolCalls.0.status" - 第一个工具调用状态变了
  // "streamingText" - 流式文本更新了
});

对比其他方案

特性 Redux Zustand MobX easy-model
类模型
无装饰器
依赖注入
撤销重做
深度监听 ⚠️
TypeScript友好 ⚠️ ⚠️

结论

AI应用前端,状态管理选easy-model就对了。


GitHub: github.com/ZYF93/easy-…

npm: pnpm add @e7w/easy-model

做AI应用前端,状态管理别再踩坑了,点个⭐️!

我用 Zustand 三年了,直到遇见 easy-model...

不是说Zustand不好,而是有些场景它真的hold不住。

故事是这样的

我们公司有个中后台项目,状态管理一直用Zustand。讲真,Zustand确实香——API简洁、性能好、类型推断也还行。

直到有一天,产品经理提了一个需求:

"做一个操作日志中心,用户每做一个操作就记录下来,支持撤销重做。而且要能在列表页看到嵌套对象的变化轨迹。"

我自信满满地开始写,然后就被打脸了。

Zustand的痛点

1. 状态一多就成了"函数大杂烩"

// store.ts
const useStore = create((set, get) => ({
  user: null,
  orders: [],
  filters: {},
  pagination: { page: 1, size: 10 },
  loading: false,

  setUser: (user) => set({ user }),
  setOrders: (orders) => set({ orders }),
  setFilters: (filters) => set({ filters }),
  setPagination: (pagination) => set({ pagination }),
  setLoading: (loading) => set({ loading }),

  fetchOrders: async () => {
    const { filters, pagination } = get();
    set({ loading: true });
    const res = await api.getOrders(filters, pagination);
    set({ orders: res.data, loading: false });
  },

  // ... 200行后
}));

一个文件写了500行,到后面自己都不想看了。

2. 撤销重做?自己实现吧

Zustand没有内置history支持。网上倒是有zundo这种中间件,但:

  • 配置繁琐
  • 类型推断经常出问题
  • 和业务代码集成麻烦

3. 监听嵌套对象?不好意思,做不到

const orders = useStore((s) => s.orders);
// 改变了 orders[0].items[0].price
// 组件不会更新!因为引用没变

你得用subscribe或者自己写selector,关键是一旦嵌套深了,selector写得怀疑人生。

然后我发现了easy-model

// 用类来组织,一个领域一个类
class OrderModel {
  orders: Order[] = [];
  filters: FilterParams = {};
  pagination = { page: 1, size: 10 };
  loading = false;

  async fetchOrders() {
    this.loading = true;
    const res = await api.getOrders(this.filters, this.pagination);
    this.orders = res.data;
    this.loading = false;
  }

  setFilter(key: string, value: any) {
    this.filters[key] = value;
  }
}

// 内置history支持
const order = useModel(OrderModel, []);
const history = useModelHistory(order);

// 撤销重做,一行搞定
history.back();
history.forward();
history.reset();

这才是面向对象!

深度监听,真香

class ComplexModel {
  user = {
    profile: {
      address: { city: "北京" },
    },
  };
  orders = [];
}

// 监听嵌套对象变化
watch(user, (keys, prev, next) => {
  // keys: ['profile', 'address', 'city']
  console.log("变化了", keys, prev, next);
});

user.profile.address.city = "上海";
// 自动触发监听,拿到完整的变化路径

还有IoC?

// 定义依赖
const apiSchema = object({
  baseUrl: string(),
}).describe("API配置");

// 注入
class OrderApi {
  @inject(apiSchema)
  config?: { baseUrl: string };

  async getOrders() {
    return fetch(`${this.config?.baseUrl}/orders`);
  }
}

// 配置
config(
  <Container>
    <CInjection
      schema={apiSchema}
      ctor={OrderApi}
      params={["https://api.example.com"]}
    />
  </Container>,
);

这不妥妥的企业级架构?

性能对比

官方benchmark(10万个元素,5轮批量更新):

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

easy-model比Zustand慢3倍,但换来了:

  • 类模型组织方式
  • 内置IoC能力
  • 深度监听
  • History支持

这波不亏!

怎么选?

  • 小项目、简单状态 → Zustand依旧真香
  • 中大型、需要领域模型、需要IoC、需要history → easy-model真香

Github: github.com/ZYF93/easy-…

觉得有帮助的点个⭐️支持下 🙏

easy-model -- "小而美"的React状态管理方案

作为一个被Redux boilerplate折磨多年的前端er,今天我要安利一个让我眼前一亮的库——easy-model。

先说说我的踩坑史

还记得当年学Redux的时候吗?

  • 先装reduxreact-redux
  • 定义action types
  • actions
  • reducers
  • 配置store
  • connect包装组件...
  • 最后还要写selectors

一个简单的计数器,写了将近100行代码。

后来转MobX,装饰器倒是爽,但TypeScript类型推来推去总出问题,而且那个隐式的依赖追踪,看代码时完全不知道谁在监听谁。

再后来用Zustand,哇,真TM简洁!但用久了总感觉缺点什么——没有类模型的组织方式,状态一多就成了"函数大杂烩"。

直到遇到easy-model

// 一个计数器,只要这么几行
class CounterModel {
  count = 0;

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

  decrement() {
    this.count -= 1;
  }
}

function Counter() {
  const counter = useModel(CounterModel, []);

  return (
    <div>
      <div>计数: {counter.count}</div>
      <button onClick={() => counter.increment()}>+</button>
      <button onClick={() => counter.decrement()}>-</button>
    </div>
  );
}

这才是人写的代码!

它到底好在哪?

1. 类模型,写起来就是爽

class UserModel {
  userInfo: User | null = null;
  loading = false;

  async fetchUser(id: string) {
    this.loading = true;
    const res = await api.getUser(id);
    this.userInfo = res;
    this.loading = false;
  }
}

字段就是状态,方法就是业务逻辑。没有actions,没有reducers,什么都没有!

2. 依赖注入,骚操作来了

// 先定义schema
const userSchema = object({
  id: number(),
  name: string(),
}).describe("用户");

// 注入一个服务
class UserService {
  @inject(userSchema)
  user?: User;
}

// 配置
config(
  <Container>
    <CInjection schema={userSchema} ctor={UserService} />
  </Container>,
);

这不妥妥的Spring Boot既视感?

3. 深度监听,想监听什么监听什么

class OrderModel {
  order: Order = { items: [], total: 0 };
  user: User = { name: "" };
}

watch(order, (keys, prev, next) => {
  // keys 是 ['items', 0, 'price'] 这样的数组
  console.log(`${keys.join(".")} 变了`, prev, "->", next);
});

嵌套对象、跨实例引用、getter返回的实例...全都能监听!

4. 性能居然比MobX还快

官方有个benchmark,10万个元素的数组批量更新5轮:

  • easy-model: ~3ms
  • MobX: ~17ms
  • Redux: ~52ms
  • Zustand: ~0.6ms(最快,但它没有IoC能力)

在有IoC能力的状态管理方案里,easy-model基本没有对手。

对比一下

特性 easy-model Redux MobX Zustand
类模型
IoC/DI
深度监听
性能 最快
学习成本

适合谁用?

  • 喜欢用类来组织业务代码的
  • 需要依赖注入的(比如仓储模式)
  • 对"监听嵌套字段变化"有强需求的
  • 不想被Redux boilerplate逼疯的

怎么入门?

pnpm add @e7w/easy-model

官方文档很详细,中英文都有。GitHub上也有完整的example和test。

一句话总结:这是近年来我用过最"对胃口"的状态管理方案。


Github地址:github.com/ZYF93/easy-… 如果觉得不错,点个star支持下呗 🙏

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

不知道你们有没有这种感觉——每次新建一个 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-…

❌