React 缺失的“M”层:我开发了 Zenith,重塑完整的 Model

2025 年 12 月 24 日
 jaydenWang

迷失的 Model

我们在谈论 React 时常说 UI = f(State)。React 完美地解决了 View (视图) 层,但对于 Model (数据模型) 层,社区的探索从未停止。

从 Redux 到 Hooks ,再到 Zustand ,我们越来越追求“原子化”和“碎片化”。这带来了极简的 API ,但也带来了一个严重的副作用:Model (模型)的破碎

你是否遇到过这种情况:

“Model” 消失了,取而代之的是散落在各处的逻辑碎片。

Zenith:重塑 Model 层

Zenith 注重于高内聚( Co-location ) 的开发体验,可以把数据 (State)计算 (Computed)行为 (Action) 紧紧地封装在一起。

Zenith = Zustand 的极简 + MobX 的组织力 + Immer 的不可变基石

核心特性:“诚实”的 Model

1. 完整的模型定义 (Co-location)

在 Zenith 中,你不需要在闭包里用 get() 去“偷窥”状态,也不用担心 set 的黑盒逻辑。一个 Store 就是一个完整的、逻辑自洽的业务单元。

class TodoStore extends ZenithStore<State> {
  // 1. 数据 (State)
  constructor() {
    super({ todos: [], filter: 'all' });
  }
​
  // 2. 自动计算属性 (Computed)
  // 告别手动写 Selector ,告别 useMemo
  // 像定义原生 getter 一样定义派生状态
  @memo((s) => [s.state.todos, s.state.filter])
  get filteredTodos() {
    const { todos, filter } = this.state;
    // ...逻辑
  }
​
  // 3. 行为 (Action)
  // 诚实地使用 this ,UI 层绝不能直接碰 State
  addTodo(text: string) {
    this.produce((draft) => {
      draft.todos.push({ text, completed: false });
    });
  }
}

2. 链式派生:自动化的数据流

MobX 最让人着迷的是它的自动响应能力。Zenith 完美复刻了这一点,但底层依然是 Immutable Data

你可以基于一个计算属性,派生出另一个计算属性( A -> B -> C )。当 A 变化时,C 会自动更新。我们不再需要手动维护依赖链,也不需要在组件里写一堆 useMemo一切计算逻辑都收敛在 Model 内部

3. 组件即视图 (View):像 Zustand 一样简单

定义 Model 虽然严谨,但在组件里使用必须极致简单。Zenith 提供了完全符合 React Hooks 习惯的 API 。

你不需要高阶组件( HOC ),不需要 Connect ,只需要一个 Hook:

const { useStore, useStoreApi } = createReactStore(TodoStore);
​
function TodoList() {
  // ✅ 像 Zustand 一样选择状态
  // 只有当 filteredTodos 变化时,组件才会重渲染
  const todos = useStore((s) => s.filteredTodos);
  
  // ✅ 获取完整的 Model 实例 (Action)
  const store = useStoreApi();
​
  return (
    <div>
      {todos.map((todo) => (
         // UI 只负责触发意图,不负责实现逻辑
        <div onClick={() => store.toggle(todo.id)}>
          {todo.text}
        </div>
      ))}
    </div>
  );
}

4. 工程化的胜利

Zenith 不仅仅是一个状态库,它内置了 History (撤销/重做)DevTools 中间件。

我用它构建了 domd markdown WYSIWYG 编辑器,能够支撑 20000 行文档流畅编辑。

结语

Zenith 的出现不是为了争论 FP 好还是 OOP 好。

它只是想告诉你:当你的项目逻辑日益复杂,当你受够了在几十个 Hook 文件中跳来跳去寻找业务逻辑时,你值得拥有一个完整的、诚实的 Model 层。

让代码重归秩序。

Github: https://github.com/do-md/zenith

欢迎 Star 🌟 和 Issue 交流!

4719 次点击
所在节点    程序员
57 条回复
kingkongdog
2025 年 12 月 24 日
恕我直言,99.99999% 的 React 项目都是贫血模型,Model 层毫无用处。
uglyer
2025 年 12 月 24 日
是个单例?
jaydenWang
2025 年 12 月 24 日
@uglyer 不是单例,也不推荐单例。使用 createReactStore 创建的都是局部状态,组件多实例彼此不影响。store 随着组件的生命周期销毁。直接 const todostore = new TodoStore()是单例,不推荐这么做,不过有些真正的全局状态可以这么干
lanten
2025 年 12 月 24 日
redux 都已经扫进历史垃圾堆了,因为这完全脱离实践,是在跟空气斗智斗勇,过度设计的典范
jaydenWang
2025 年 12 月 24 日
@lanten 不需要写模版语言,组件层使用起来跟 zustand 是一样的。额外支持:1. 默认局部状态,支持多实例; 2. 计算属性。3.复杂状态性能优势明显 store 是纯粹的 class 写法,没有额外的约束
shunia
2025 年 12 月 24 日
能不能不要设计得这么复杂。。。zustand 一个 object 定义完整 store 逻辑的方案显然更简洁清晰一些,又不是非要用 this ,用 get()又不犯法。
而且你的实现好像也并没有解决你说得问题啊,看起来只是用 class 的形式重新实现了一遍 zustand 。
rich1e
2025 年 12 月 24 日
20000 行文档流畅编辑,跟 Model 有关系吗?🤔
ltaoo1o
2025 年 12 月 24 日
理想是好的,现实不需要,开发者的水平 + 业务的快速迭代 or 复杂变更,注定了不会被接受。经历过好几个公司,历史代码都能看到类似的自研项目,当时的开发者走了,后面就不会再用了。包括 dva 这种之前流行的方案,新代码也不会用了
mistsobscure
2025 年 12 月 24 日
这对吗
jaydenWang
2025 年 12 月 24 日
@shunia 1. 解决了 zustand 没有计算属性的问题,有了计算属性,就不需要在组件层 selector ,可以把所有状态内聚的 store 中,计算属性 store 内以及各个组件都可以复用。可以做到没有 UI 的情况下,完成完整的业务逻辑
2. 把“set”方法保护起来,组件中是无法 set 的,可以自由读取状态,set 状态必须调用 store 的 action
jaydenWang
2025 年 12 月 24 日
@rich1e 这是 Zenith 的性能优势。借助 immer 的不可变状态,共享引用以及 Zenith 的计算属性,可以实现更改一个深度状态,只渲染状态树的这条分支
gkinxin
2025 年 12 月 24 日
你这案例一个 useState 都写完了。
zzlove
2025 年 12 月 24 日
简单业务我觉得这样更简单直观,Store 代码算是 ts 类型标注就几十行
jaydenWang
2025 年 12 月 24 日
@gkinxin 这一块示例不完整,github 有完整的示例。多个组件如何读取状态、set action
novaline
2025 年 12 月 24 日
RTK 足矣,不要造轮子了
mrwangjustsay
2025 年 12 月 24 日
jaydenWang
2025 年 12 月 24 日
@novaline 没有重复造轮子,核心是 immer 。github 有跟 RTK 的对比
pakholeung372
2025 年 12 月 24 日
export class Service implements IServiceWithStore<State> {

store

useState

setState

getState

constructor() {
this.store = create(
() => ({
current: undefined,
}),
)

this.useState = this.store
this.setState = this.store.setState
this.getState = this.store.getState
}

getCurrent(state = this.getState()) { return state.current }

useCurrent() {
return this.store.useState(this.getCurrent)
}
}

我一般是这样写的,就是会有一些样板代码
jaydenWang
2025 年 12 月 24 日
@pakholeung372 思路很像,一开始也写过 this.store = create(
() => ({
current: undefined,
}),
)
BingoXuan
2025 年 12 月 24 日
大部分情况下都是因为数据职责划分问题。不在于工具,而在于设计。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1180806

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX