You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

公交时刻表应用:数据修改代码应放函数还是仅在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调用的问题:

  1. 优化纯数据修改函数:继续维护针对单个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
}
  1. 封装通用修改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])
  })
}
  1. 简化组件调用逻辑:组件中直接调用通用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 06:53:15