公交时刻表应用:数据修改代码应放函数还是仅在actions中?
公交时刻表应用的Flux架构状态管理最佳实践
我正在开发一款公交时刻表应用,用对象类型建模时刻表文档:
interface Timetable { name: string stops: string[] services: string[][] }
配合该类型,我写了若干数据修改函数(如果用突变写法,这些函数通常会作为类方法),主要借助Immer避免大量扩展语法,例如:
const addStop = (timetable: Timetable, stopName: string): Timetable => { return produce(timetable, (newTimetable) => { newTimetable.stops.push(stopName) }) }
我用Zustand和Immer管理状态(换成Redux问题也类似),store里有一个Timetable对象数组,还有用Immer重新分配当前选中时刻表的action:
updateTt: (tt, index) => { set((state) => { state.timetables[index] = tt }) }, updateThisTt: (timetable) => { set((s) => { if (s.selectedTtIdx === null) { throw new Error("no selected timetable") } s.timetables[s.selectedTtIdx] = timetable }) },
我在React组件中调用数据修改函数,再调用更新action:
const onAddStop = (name) => { updateThisTt(addStop(timetable, name)) }
这种写法可行,但我不确定是否合理:存在两层Immer调用,组件直接在事件处理器中调用数据修改函数,且函数写法不够美观。
我考虑过三种方案:
- 将所有数据修改逻辑移至actions中,但这会增加索引数组的重复代码,降低可维护性和可读性;
- 为每个数据修改函数对应创建一个action,从事件处理器调用这些action,但会产生命名等层面的重复;
- 将Timetable类型改为类,将数据修改函数改写为突变方法,设置
[immerable] = true让Immer在action中处理所有逻辑,但我更倾向于使用不可变记录模式。
Flux、Zustand或Immer的文档通常展示第一种方案,但实际应用远不止counter = counter + 1这么简单。请问在实际使用Flux架构的应用中,最佳结构是什么?
实际项目中的最佳实践方案
结合需求和三种方案的优缺点,推荐提取Timetable的纯修改逻辑到独立工具函数,同时在store中封装通用的"修改选中时刻表"的高阶action,既避免重复代码,又保持不可变模式的优势,同时解决两层Immer调用的问题:
- 优化纯数据修改函数:继续维护针对单个
Timetable的纯函数,但改为直接接收Immer的draft对象操作,无需外层produce:
import { Draft } from "immer" // 直接操作draft,无需返回新对象 const addStop = (timetableDraft: Draft<Timetable>, stopName: string) => { timetableDraft.stops.push(stopName) } const editStopName = (timetableDraft: Draft<Timetable>, oldName: string, newName: string) => { const index = timetableDraft.stops.findIndex(s => s === oldName) if (index !== -1) timetableDraft.stops[index] = newName }
- 封装通用修改action:不用为每个数据操作单独写action,而是写一个接收"修改器函数"的通用action,统一处理索引和选中状态逻辑:
// Zustand store中的action modifySelectedTt: (modifier: (draft: Draft<Timetable>) => void) => { set((state) => { if (state.selectedTtIdx === null) { throw new Error("no selected timetable") } // 直接在state的draft上调用修改器,仅一层Immer处理 modifier(state.timetables[state.selectedTtIdx]) }) }, // 针对指定索引的通用修改action modifyTtAt: (index: number, modifier: (draft: Draft<Timetable>) => void) => { set((state) => { modifier(state.timetables[index]) }) }
- 简化组件调用逻辑:组件中直接调用通用action并传入对应的修改器函数,无需手动处理Immer或对象替换:
const onAddStop = (name) => { modifySelectedTt((draft) => addStop(draft, name)) } const onRenameStop = (oldName, newName) => { modifySelectedTt((draft) => editStopName(draft, oldName, newName)) }
方案优势
- 避免重复代码:不用重复处理数组索引、选中状态判断,也不用为每个操作单独写action;
- 消除多层Immer调用:所有修改都在store的Immer上下文里完成,组件无需额外调用produce;
- 保持不可变模式:依然用纯函数处理数据修改逻辑,无需引入类和突变方法;
- 职责清晰:Timetable的业务修改逻辑集中在工具函数,store只负责状态的定位与更新,代码维护性更强。
这种模式在中大型Flux/Redux/Zustand应用中非常常见,既兼顾了文档示例的简洁性,又能应对复杂的数据结构操作。
内容的提问来源于stack exchange,提问作者Lina
相关产品推荐
相关产品推荐

