Langfuse 前端大型功能架构:用本地 Zustand Store、无 useEffect 数据流与虚拟列表治理高状态密度界面
【免费下载链接】langfuse🪢 Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. 🍊YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse
Langfuse 的前端包含 traces/observations 大表、sessions 事件流、experiments、prompts、evals、datasets 等大量"高状态密度"界面:行选择、筛选器、抽屉、懒加载、批量操作、虚拟滚动全部集中在少数巨型组件里。本文基于 Langfuse 仓库中 frontend-large-feature-architecture 技能文档 及其引用的参考文档,完整拆解其"大型前端功能架构"方法论:控制器(controller)与视图分离、本地 vanilla Zustand store 的创建与订阅规范、"无 useEffect"的派生数据流、虚拟列表的行边界治理,以及面向遗留代码的分片迁移路径。读完本文,你能掌握在类似 Langfuse 这类数据密集型 React 应用中设计状态边界、拆分重组遗留巨型组件的完整实操方案。
1. 适用场景与核心概念:什么是 "controller"
该技能定义了自己的触发场景:构建、修改或重构大型前端界面时使用,尤其涉及虚拟化列表、大表格、控制器组件、Zustand store、行选择、高频状态、渲染性能,或者在添加useEffect/useMemo/useCallback、从加载数据派生表单默认值之前。
文档给出了关键术语的严格定义:
在本技能中,"controller" 指拥有功能逻辑的组件或 hook:数据获取、视图状态、表格/列表状态、effects、actions 以及昂贵渲染。问题不在名字,而在于一个地方拥有了太多可变职责。
这个定义是整篇方法论的锚点:Langfuse 的 traces、observations、experiments、prompts、evals、datasets、session 视图都存在"controller-heavy"界面,仓库中仍有数百处待迁移的useEffect(文档在撰写时统计web/src中有超过 350 个useEffect调用点)。技能文档明确要求:不要把现有大型 Langfuse 功能组件当作范例来复制,除非它们已经遵循该技能,否则一律视为迁移候选对象。
完整的规则清单见 big-feature-rules.md。
2. Big Feature Rules:所有权基线与十条硬规则
2.1 所有权基线(Ownership Baseline)
规则文档首先给出状态归属的基线,这是所有后续判断的"法庭":
| 状态类别 | 归属位置 |
|---|---|
| 页面生命周期 / 功能域依赖 | 页面/视图组件(page/view)创建并拥有 |
| 服务端/查询状态 | tRPC / React Query |
| 路由状态 | router / filter hooks |
| 高频本地 UI 状态 | 每次挂载一个的本地 feature store |
| 跨路由/跨功能的产品级状态 | 全局 store(仅此用途) |
| 纯数据准备 | 独立纯函数,在渲染前把"抓取的数据"编译成"UI 数据" |
| 复杂用户工作流 | actions/*.ts文件或 store actions |
| Effects | 集成边界(外部系统对接),不是常规状态推导手段 |
| 昂贵的行/单元格/条目 | 视图只读(view-only),背后是窄容器 |
数据流被要求单向:抓取的数据 → 编译后的 UI 数据 → 渲染。
2.2 十条硬规则(Hard Rules)
- 功能生长通常意味着需要把 React 渲染与功能逻辑分离——更大的组件不是架构。
- 大多数组件应当是 view-only:要么渲染被给定的东西,要么从 context 可用的 feature store 中读取一小块选中的值。
- 复杂的数据准备放在独立纯函数中,后端数据单向流动。
- 触发复杂逻辑的用户操作放在渲染组件之外,作为命名异步函数或 store action;action 需要大量上下文时,传入 feature store 实例让 action 自己取所需。
- 同时存在本地 store 状态与 React Query 数据的功能,需要显式桥接(自定义 hook 或命名 action)。
- 页面实例级状态(筛选器、选中行、展开行、懒加载状态、抽屉、本地 actions)优先用 view-scoped 本地 store,而不是全局 store。
- 本地 store 实例用lazy
useState初始化器创建,不用useMemo——store 是每次挂载的实例,不是渲染期派生值。 - Effect 不应是常规状态变更器。添加 effect 前先尝试:纯数据准备、store action、显式事件处理器。
- 不要复制现有大型 Langfuse 功能组件作为范例;除非遵循本技能,否则按遗留处理。
- 遵循 react-without-useeffect.md 中的黄金示例:数据未就绪时不渲染 UI、从
initialValueprop 播种状态、在渲染中派生值而不是用 effect 同步;当改动改善了功能边界时,更新该功能的 README 或迁移笔记,记录改了什么、下一片是什么。
2.3 Effects 与 Actions 的边界
规则文档进一步划界:
- Effect 只用于命名的外部系统集成:订阅、observer、浏览器事件监听器、定时器、命令式第三方 API,及其清理函数。放在容器或 feature hook 中,不放视图组件里;如果某个 effect 反复写状态,就把对应 store action 做成幂等的。
- 复杂 action 必须可以在不渲染组件的情况下被调用。组件负责接线 hooks 并传入依赖,action 拥有工作流本身。文档给出的形态:
await applyBulkAction({ store, queryClient, projectId, });- Action不得调用 React hooks:把 hook 结果、query 辅助函数、本地 store 实例或窄回调依赖传进去。若 action 需要大量数据准备,在旁边导出一个纯 helper,让转换可以独立测试。
- 反面模式同样被点名:不要为了让一个按钮能做功能级工作而穿过整棵树传 20 个 props;也不要因为页面 controller 恰好拥有全部依赖,就把复杂工作流内联在页面里。
3. React Without useEffect:状态 → 帧的心智模型
这是该技能的"黄金模型"文档(react-without-useeffect.md),核心命题是:UI 是状态的纯函数。
3.1 为什么"写状态的 effect"在机制上必然坏
文档用游戏引擎类比:UI 是一帧,由纯函数从状态渲染。effect 写状态会机械性地破坏这个模型:
- React 用不完整状态commit 并绘制一帧;
- effect 在绘制后触发,二次渲染"修复"这帧;
- 两帧之间用户可以操作、异步结果可以到达——修复步骤与真实输入产生竞态。
这就是"effect 重置了我的表单"这类 bug 的来源。React 18+ 的并发渲染让时序更不可预测,但模型本身已经是坏的:本应一次纯渲染的地方变成了两次渲染加一个修复步骤。原则是:保持渲染纯,逻辑放到执行同步且归属明确的地方——事件处理器、命名 action、普通派生值。
3.2 头号反模式:effect 派生/准备数据
文档引用了仓库中的真实案例 EditMonitorPage.tsx:
const [liveName, setLiveName] = useState(""); useEffect(() => { setLiveName(data?.name ?? ""); }, [data?.name]);一个 effect 把抓取到的数据镜像进本地状态,制造出两个事实来源、两者之间的时间窗口,以及竞态:用户在查询落定前编辑了名字,effect 会覆盖他的输入。修复方式取决于值的性质:
- 纯派生值(本地永不编辑):在渲染中计算,无状态无 effect:
const title = data?.name ?? ""。 - 加载数据播种可编辑状态(此案例):用下面的"黄金拆分"。
3.3 黄金示例:拆分组件,用加载状态门控渲染
拆成两个组件。外层是数据准备器 + 控制器:拉取数据,查询挂起期间渲染 loading 态(优先匹配最终布局的 skeleton,<Spinner />是最低限度的兜底)。内层接收加载好的值作为initialValueprop,用useState(initialValue)播种——值保证已存在且稳定,没有任何 effect 能覆盖它:
// 外层:数据准备器 + 控制器。数据未就绪前不渲染 UI。 function EditMonitorPage() { const { data, isPending } = api.monitors.get.useQuery({ projectId, id }); if (isPending) return <Spinner />; if (!data) return <ErrorPage title="Monitor not found" />; return <EditMonitorForm key={data.id} initialMonitor={data} />; } // 内层:仅在数据存在后挂载。状态只播种一次。 function EditMonitorForm({ initialMonitor }: { initialMonitor: Monitor }) { const [liveName, setLiveName] = useState(initialMonitor.name); // ... }这从根上消灭了"加载前输入"的整类竞态:不存在表单没有数据就存在的时刻。key={data.id}在实体身份变化时重新挂载内层组件,保证播种始终诚实。
表单规则:只有在所有初始值都已准备好之处才定义表单;数据还在加载,表单就放在更深的树里——DataPreparerAndController拉取、显示 loading,然后把initialValues和onSubmit传给只负责渲染字段与提交的纯FormComponent。文档举了仓库中的真实案例 CreateOrEditLLMToolDialog.tsx:useForm在对话框顶部用 create 模式默认值创建,之后一个"编辑模式下填充表单"的 effect 在现有工具可用时调用form.reset(...)。正确的修复是结构性的而非"更好的 effect":在渲染表单组件之前决定defaultValues(create vs edit),只在就绪时渲染表单,reset effect 随之消失。
3.4 从服务端状态派生客户端状态
当客户端状态引用服务端数据(选中的行、active id、选中的选项)时,只存用户意图(id),在渲染中通过与 query 数据合并派生有效值:
const selectedId = useFeatureStore((s) => s.selectedId); const selected = rows.find((r) => r.id === selectedId) ?? null;不要写一个 effect 在 refetch 时校验或把服务端数据拷贝进客户端 store。派生模式让每个事实只有一个事实来源——store 拥有意图,query 拥有数据——合并在 loading、refetch、失效过程中始终正确。只有当合并可测量地昂贵时才包useMemo。
3.5 什么时候 useEffect 才合法
Effect 用于触达 React 渲染之外的东西:DOM 与浏览器 API——document.addEventListener/removeEventListener、observers、命令式第三方 API、Web Audio(且需要用户手势)。健康的 effect 具备三要素:无依赖(或极少的稳定依赖)、单一关注点、有清理函数。
3.6 useCallback / useMemo 是过早优化
useCallback就是函数版的useMemo。不要在确认并测量过的性能问题之前做记忆化;拆分组件就是记忆化的一种形式。到处都需要记忆化说明组件重渲染过于频繁,意味着状态边界错了——修边界。唯一例外是把引用稳定性当作正确性要求:当回调或对象是一个合法的集成 effect 或数据获取 hook 的依赖时,记忆化它(或提升到组件外),避免依赖每次渲染变身份、导致 effect 循环——这是正确性,不是优化。
3.7 优先 UI 方案而非代码方案
很多前端问题有 UI 解而不是代码解:loading 态加门控渲染,比"保持行为的聪明重构"删掉的复杂性更多。不要害怕改变行为来简化——在页面之前显示半就绪表单的地方渲染 skeleton 是改进而非回归。这也是为什么"先写特征测试再保持行为重构"很少适用于大型前端重构:目标行为往往就是更简单的那个,而不是当前的那个。
4. 本地 Feature Store:默认模式、创建方式与反模式
local-feature-state.md 规定了本地状态的完整治理方案。
4.1 目标与默认模式
大型前端功能不应让一个组件或 hook 拥有一切——那种形态下,一个复选框、hover 或行选择变化会重跑构建筛选器、列、数据包装器、昂贵单元格、抽屉、actions 和路由粘合代码的同一份代码。目标是让每个状态变化只唤醒语义上依赖它的 UI 与 actions。
默认模式七步:
- 页面/视图用 lazy
useState创建一个 store 实例; - Context 只提供稳定的 store 实例;
- 组件订阅最小的有用切片;
- 变更逻辑放在命名 store actions 中;
- 复杂用户工作流放在
actions/*.ts或 store actions 中; - 昂贵单元格/行保持在窄容器之后的 view-only 状态;
- 大型功能根目录有一个简短的
README.md所有者地图。
只在状态高频、被挂载功能内多个子树共享、或必须跨行/条目重挂载存活时才加本地 vanilla Zustand store。
4.2 为什么是本地而不是全局
Langfuse 页面天然本地状态密集:筛选器、保存视图、选中行、展开行、抽屉、peek 导航、懒行加载状态、视图本地 actions——这些状态通常属于一个已挂载的页面实例,而非整个应用。本地 store 在页面/视图中创建、卸载时销毁,让多个已挂载实例相互独立、避免跨路由状态泄漏,并使所有权可见。全局状态保留给真正跨功能/跨路由的产品状态;不要为了逃避 prop 钻取或让大组件变小而把状态提升为全局。同样,不要为了让 PR 看起来有架构而加 store——如果当下问题是内联导出工作流、重复的筛选选项整形或庞大的列构建器,先抽 action 或纯 helper;只有当状态需要选择性订阅或必须在一个已挂载功能实例内跨行重挂载存活时才用 store。
4.3 Store 创建:lazyuseState而非useMemo
const [store] = useState(() => createFeatureStore({ initialProjectId: projectId, }), );这表达了真实生命周期:一个 store 实例对应一个已提交的已挂载视图,未使用的 setter 可以接受。useMemo是错的默认值——它是渲染期派生值的缓存,不是外部 store 实例的所有权边界;本地 store 是有状态的基础设施,其身份应当按状态对待。useRef也能持有稳定实例,但除非命令式 setup 需要 ref,否则优先useState。保持 store 创建纯函数;变化的路由/查询输入用命名 store action(如resetForFeature(...)或init(...))同步进去。
4.4 Store 形状与订阅
用不可变纯对象承载需要 selector 友好的键控状态:
type FeatureStoreState = { selectedIds: Record<string, true>; activeId: string | null; actions: { toggleSelected: (id: string, selected: boolean) => void; setActiveId: (id: string | null) => void; }; };避免原地变更Set/Map;若使用,更新时整体替换实例。selector 应返回原始值或稳定引用;一个组件需要多个值时用 shallow selector 辅助或拆分订阅,让一个字段的变化不重渲染无关 UI。
4.5 独立 Actions
组件接线 hooks、路由参数、store、analytics 与 query 辅助;action 拥有工作流:通过回调 refetch、读取传入的 store、调用纯 helper、执行浏览器副作用、发 analytics。Action 不得调用 React hooks;工作流需要大量上下文时,传入本地 store 实例或小型依赖对象,而不是穿过视图组件传递长 prop 链:
await exportFeatureData({ capture, fetchDetails, projectId, refetchSummary, selectedIds, });对大量数据整形,在旁边导出纯 helper,让转换无需渲染页面即可测试。
4.6 反模式清单
- 表格/列表组件同时拥有选择、筛选、列、路由、peek 状态、批量操作、本地对话框和昂贵渲染单元格;
- Context provider 的
value随行选择、hover、滚动、active 行、展开行等高频状态变化; - 只属于一个已挂载页面实例的本地 feature 状态被提升为全局 store;
- 用
useMemo拥有本地外部 store 实例; - 共享的
src/components/*组件调用 feature-scoped store hook——这会悄悄破坏未挂载该 feature provider 的其他调用方(view-scoped 的 Zustand 消费者必须放在src/features/*); - 记忆化是唯一修复手段;
- 回调依赖内联配置对象,每次渲染身份都变;
- 组件只需要一个布尔值却订阅大对象;
useEffect从抓取数据派生常规 UI 状态;- 为了一个按钮能做 action,组件用长 prop 链传 feature context;
- 页面组件把所有复杂异步工作流内联,因为 hooks 恰好都在作用域里。
4.7 本地状态迁移十一步
- 先埋点:识别哪个语义状态变化引起大范围渲染;
- 选最小有用边界:store 状态、action 工作流、纯数据准备、命令式集成 hook;
- 仅在需要选择性订阅时把该状态组抽入本地 store;
- 用最小 UI 边界上的 selector 订阅替换宽 prop;
- 相关变更移入命名 actions;
- 稳定回调与数据包装器;
- 昂贵数据准备移入纯函数;
- 复杂用户 action 移入 store actions 或
actions/*.ts外部函数; - 更新 feature README:改进了什么、还剩什么分散状态、下一个原子切片;
- 移除调试埋点;
- 对下一个状态组重复。
5. 虚拟化列表:渲染边界基础设施
virtualized-lists.md 针对 Langfuse 实际使用的@tanstack/react-virtual(web/package.json 中声明为^3.13.12)给出专项规则。核心定性:虚拟化列表是渲染边界基础设施——它只应计算哪些 item shell 可见并定位它们,不应让每一行拥有 feature 状态、effects、订阅、数据加载和工作流。修改虚拟化界面前先找出当前调用点,然后套用与任何大型功能相同的状态边界规则。
5.1 "智能陷阱"(Smartness Trap)
文档描绘了损坏的完整链路:
- virtualizer 在滚动时重渲染;
- 父组件重建 callbacks、config、行包装器或数据对象;
- 行组件收到变化的 props,尽管语义行没变;
- 行本地 effects/加载状态被重置或重新触发;
- 动态测量观察到了被外部变更者(如 Google 翻译)改过的 DOM;
- 测量更新 virtualizer 状态,又把同一批行重渲染一遍。
"这不是一个小的记忆化 bug,这是泄漏的状态所有权。" 修复方向:让滚动与测量状态只更新尽可能小的集成边界——滚动改变 virtual item offset 时,未变化的行内容不应收到新的语义 props、不应重触发 effects、不应重建昂贵派生数据。
5.2 Google 翻译的 DOM 行为
浏览器翻译会在 React commit 之后变更已渲染 DOM:包裹文本节点、替换文本、改变元素尺寸——全部在 React 数据流之外。React 与 TanStack Virtual 无从得知这些 DOM 变化代表稳定的翻译内容还是瞬态变更。文档的明确立场:不要用translate="no"把产品 UI 排除在翻译之外(除非产品明确如此选择),Langfuse 必须能在浏览器翻译下正常工作。
5.3 测量规则(Measurement Rules)
- TanStack 视为 item 的行元素上必须放正确的
data-index; - 文本密集型行上不要把活的
measureElement与外部变更的翻译 DOM 混用; - 简单行优先固定估值 + overscan;
- 动态文本密集型行用受控测量:
ResizeObserver读行 shell、debounce 提交、活跃滚动期间不提交、高度取整避免亚像素抖动、调用virtualizer.resizeItem(index, height);若一行在两个高度间反复交替,钳制到仍允许后续合法增长的最小高度。
5.4 行规则(Row Rules)
- Virtualizer 只拥有定位;必须跨重挂载存活的状态放在行实例之外;
- 昂贵行内容是记忆化的视图组件,只接收稳定 props、不执行 effects;
- 窄行容器可以订阅本地 store 切片与 queries;
- Feature-scoped 行容器放在
src/features/*;共享的src/components/*行导出必须 context-free; - 滚动可以重渲染 virtualizer,但不应重渲染未变化的昂贵行内容;
- 不要用全局 store 在虚拟化间保存行状态——用已挂载列表/页面实例拥有的 view-scoped store。
5.5 虚拟化迁移步骤
- 加临时日志判断滚动引起的是重挂载、prop 变化、测量循环还是 query refetch;
- 发布前移除日志;
- 必须跨虚拟化存活的行本地状态移入本地 store;
- 稳定传给行的回调与 config 对象;
- 用固定估值或受控测量替换活的
measureElement; - 行逻辑移入纯 helper 或 feature 本地容器;
- 用开启浏览器翻译、水平缩放、小垂直滚动增量验证。
6. Controller 迁移指南:把"巨型组件"变成"受管功能"
controller-migration.md 给出把已长成 controller 的前端界面迁到目标形态的完整路径。目标是清晰的所有权,不是"全部塞进 store"。
6.1 每个迁移 PR 必须书面记录
- 本次针对的 controller 问题;
- 正在改进的状态/action/数据准备/渲染边界;
- 哪些行为是故意改变的、哪些必须保持不变——把渲染门控在 spinner/skeleton 后面的 UI 简化往往就是正确决定;
- 什么埋点或测试证明这个切片有效;
- 功能中仍分散着什么;
- 下一个推荐切片。
"这就是大型功能变成受管功能的方式。feature README 是活的所有者地图,随功能推进原地更新。"
6.2 十四步迁移路径
- 映射 controller:列出组件拥有的每个状态组、query、派生值、effect、回调、action 和昂贵子渲染;
- 分类状态:区分 server/query、路由、持久化浏览器、高频本地 UI、派生视图数据、命令式集成、一次性 modal/表单状态;
- 埋点一个症状:挑一个具体交互(行选择、滚动、筛选变化、抽屉打开、表单步骤变化、保存视图变化),测量什么被重渲染、重挂载、refetch、重算;
- 选边界:从高价值边界开始——选择、懒行状态、批量 action 工作流、筛选目标映射、列/视图状态、向导步骤状态、纯数据准备;
- 选最轻工具:纯 helper 或 action 抽取可能是正确第一步;只在需要选择性订阅或按挂载存活时才加本地 store;
- 需要时创建本地 store 实例(lazy
useState),context 只提供这个稳定实例; - 变更移入命名 actions;组件负责用户事件而非工作流;
- 容器与视图分离:容器可订阅、调 hooks、拉数据;视图渲染 props 或极小的选中值;
- 数据准备移出渲染:昂贵转换放纯函数,后端数据流入编译后的 UI 数据再进入渲染;
- 显式桥接 query 状态:本地 store 决策依赖 React Query 数据时,用命名 feature hook 或 action 表达该关系;
- 隔离命令式集成:虚拟化器、observers、键盘监听、第三方 DOM 变更处理、定时器都放在窄集成 hook 里;
- 更新 feature README:记录本 PR 改进了什么、期望边界、已知的分散状态、下一个抽取目标;
- 移除调试埋点;
- 重复:每个切片让一个语义交互变窄、更易推理。
6.3 按功能选第一个切片
- Traces 与 observations 表:从行选择、全选、批量 action 或昂贵单元格包装器开始;
- Session 详情与 session 事件:把虚拟化、懒行加载状态、动态测量与渲染行内容隔离开;
- 实验结果表:选中行状态、筛选目标映射、run/evaluation 批量 action、比较列构建的纯 helper;
- 实验创建向导:把已提交表单数据与显示状态(active step、选中 prompt 标签、schema 显示名、评估器选择)分开;
- Prompt 管理:拆分 prompt 详情路由/query 状态、label/version 选择、prompt 历史数据准备、mutation 工作流;
- Eval 模板与评估器表单:先抽表单默认值、model/provider 准备、校验 helper、提交工作流,再加 store;
- Datasets 与 dataset runs:隔离 active-cell/比较字段状态、表格选择、run 比较准备、上传/导入工作流。
若某个功能已有部分 hooks,视为部分迁移,不是整个界面健康的证明。
6.4 验收标准与禁忌
验收标准:本地状态变化只唤醒选中了该状态的组件;页面组件不再因无关的行级变化重建列、筛选器、行包装器与 action 回调;昂贵行/单元格是窄容器后的 view-only;effect 是外部系统集成边界而非初始化或常规数据推导;复杂工作流可以在不渲染页面的情况下被调用;feature README 告诉下一个开发者什么改进了、什么仍分散、下一个改进应瞄准什么。
明确回避:用巨型全局 store 替换巨型组件;没有测量验收标准就一次搬完所有状态;把记忆化当架构;把 provider 耦合的 store hook 放进共享src/components/*导出;因为页面恰好拥有全部依赖就把工作流留在页面内;把 README 当胜利宣言而掩盖残余 controller 状态——剩余债务必须写明。
7. Feature README:面向人与 Agent 的所有者地图
feature-readmes.md 规定大型前端功能文件夹应有简短README.md作为人和 agent 的所有者地图——优先README.md以匹配web/src/features/*现有约定,仅当文件夹已有面向用户或生成的 README 时才用FEATURE.md。README 不是 changelog,只描述持久边界并指向更深的迁移笔记。
必需章节:
- Surface:该文件夹拥有的产品界面;
- Entry Points:挂载该功能的路由文件或父组件,及它们调用的页面/视图生命周期 owner 文件;
- Structure:每个子文件夹拥有什么;根
components/只放功能内跨界面复用的组件;页面 controller、本地 store、actions/、集成 hook、界面私有容器放在detail/等界面文件夹里; - External Consumers:导入这些组件的其他功能——保持共享导出 context-free,防止意外的 provider 耦合;
- State Ownership:server/query 状态、路由状态、本地 feature 状态、全局产品状态、DOM 集成状态、view-only props 各自在哪;
- Performance And Stability Boundaries:哪些交互高频或外部不稳定、哪些组件允许因它们重渲染或测量(典型:滚动与虚拟化更新、行选择/hover/展开/懒加载、筛选/保存视图/列状态变化、抽屉/peek/键盘导航、浏览器翻译等第三方 DOM 变更、resize 与动态行测量);
- Migration State:已改进什么、哪些状态/actions 仍分散、接下来一到两个原子切片;
- Development Context:扩展该功能前应读的 agent skills、迁移笔记或 issue 文档。
更新风格要求"原地更新"而非 changelog,例如:
- 当前形态中已改进:本地 store 拥有行选择;导出 action 移入
actions/exportFeatureData.ts;行视图不再订阅筛选状态。 - 仍分散:保存视图状态仍在页面 controller;筛选选项准备仍内联;mutation 工作流仍闭包页面 hooks。
- 下一切片:筛选选项准备抽成纯 helper;批量 action 工作流移入 action 文件;路由/query 粘合与视图组件分离。
README 更新应与实际变更匹配:状态/action 抽取只更新相关迁移状态条目;新功能文件夹结构则要在逻辑移入前先写所有者地图与外部消费者。它对迁移中的功能必须诚实:不要把部分迁移的功能呈现为最终模式,不要等到完美重组才记录现状。
8. 总结:Langfuse 前端架构方法论的完整图景
把 SKILL.md 的"愿景"段落作为收束:
抓取的数据单向流动:query → 准备好的数据 → 渲染,以就绪状态门控。状态从 props 播种、被处理器编辑、永不被 effect 镜像。Actions 是树外普通函数。留下的 effects 是带清理的薄 DOM/浏览器集成。仍有数百处不符合这个形态——当你碰到其中一处,把它移向这个形态,而不是扩展那个 effect。
整篇技能文档的方法论可以压缩为一张分工表:页面/视图拥有生命周期;React Query/tRPC 拥有服务端状态;router/filter hooks 拥有路由状态;每次挂载的 vanilla Zustand store 拥有高频本地 UI 状态(selector 返回原始值,跨行重挂载存活);actions/*.ts与 store actions 拥有复杂工作流;纯函数拥有数据准备;窄集成 hook 拥有虚拟化和 DOM 集成;feature README 拥有迁移计划。仓库中 refactor-react-effects/SKILL.md 提供配套的 effect 审计与组件拆分配方,可作为本方法论的执行工具。对维护 Langfuse 或任何同类数据密集型 React 产品的前端团队而言,这套规则的实用价值在于:它不是抽象品味,而是给出了从埋点、分片、验收标准到文档留痕的完整可执行流程,让"巨型 controller"这种普遍存在的技术债第一次变成可以被一个切片一个切片地偿还的工程对象。
【免费下载链接】langfuse🪢 Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. 🍊YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考