资讯动态

Langfuse 前端架构实战:用纯渲染取代 useEffect 的派生型副作用

发布时间:2026/9/10 8:49:08 来源:尧图企业网站定制
Langfuse 前端架构实战用纯渲染取代 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/langfuseUI 是状态的纯函数。Langfuse 前端团队在 react-without-useeffect.md 中确立了这一架构准则凡是派生数据、准备数据、同步数据的useEffect都不应该存在。本文以该文档为骨架结合 Langfuse 仓库web/src中真实组件监视器编辑页、LLM 工具对话框的现状与迁移方向完整讲解State → Frame心智模型、按加载状态门控渲染的黄金拆分模式、表单初始值处理、动作外置、useEffect的合法边界以及useCallback/useMemo的过度优化问题帮助你在大型前端功能中写出无派生型副作用、单真相源、可直接测试的 React 代码。为什么 React Without useEffect 是一份工程准则Langfuse 的前端代码库中useEffect的调用点数以百计文档写作时web/src下超过 350 处。Agent 与开发者因为训练数据中充满了useEffect会条件反射式地用它解决一切数据变化后要做点什么的问题——但绝大多数这种 effect 都属于派生、准备或同步数据的范畴应当被消灭而不是被继续使用。这份文档被团队称为黄金范例与目标形态the golden example and the target shape任何你触碰到的数据派生型 effect都被视为一个待迁移的候选。它不是一个建议而是大型功能架构的硬性边界。心智模型State → Frame把 React 想象成一台游戏引擎UI 是一帧画面frame由一个纯函数从状态state渲染而来。用 effect 去写状态会在机制上破坏这个模型React 用不完整的状态提交并绘制了一帧effect 在绘制完成后才触发第二次渲染修复了这一帧。在这两帧之间用户完全可以做出真实输入异步结果也可能落地于是修复步骤与真实输入形成竞态——这就是effect 把我的表单重置了这类 bug 的根源。React 18 的并发渲染让时序更加不可预测但模型本身早已是坏的本应一次纯渲染完成的事变成了两次渲染 一次修复。正确的结论是保持渲染纯净。逻辑应当放在执行是同步且被拥有的地方——事件处理器、命名动作named actions、或普通的渲染期派生值。头号反模式用 Effect 派生或准备数据文档给出的真实反模式案例位于 EditMonitorPage.tsxconst [liveName, setLiveName] useState(); useEffect(() { setLiveName(data?.name ?? ); }, [data?.name]);这个 effect 把拉取到的数据镜像mirror进了本地 state。由此产生的问题两个真相源data?.name与liveName可能不一致时序窗口两者之间存在一个同步间隙竞态如果用户在查询落定之前就编辑了名称effect 会覆盖clobber用户的输入。根据值的使用方式有两种修复路径纯派生值本地永不编辑直接在渲染期计算不设 state、不写 effectconst title data?.name ?? 用加载数据播种可编辑 state本例属于此类使用下面的黄金拆分。黄金范例拆分组件用加载状态门控渲染把单个组件拆成两个外层组件数据准备者 控制器。负责拉取数据在查询进行中渲染加载状态——优先渲染与最终布局一致骨架屏skeletonSpinner /只是最小兜底内层组件把已加载的值作为initialValueprop 接收并用useState(initialValue)播种——值保证存在且稳定没有任何 effect 能覆盖它。// 外层数据准备者 控制器。数据就绪前不要渲染 UI。 function EditMonitorPage() { const { data, isPending } api.monitors.get.useQuery({ projectId, id }); if (isPending) return Spinner /; if (!data) return ErrorPage titleMonitor not found /; return EditMonitorForm key{data.id} initialMonitor{data} /; } // 内层仅在数据存在后挂载。恰好播种一次 state。 function EditMonitorForm({ initialMonitor }: { initialMonitor: Monitor }) { const [liveName, setLiveName] useState(initialMonitor.name); // ... }这个模式消灭了整类加载前输入的竞态不存在表单已存在而数据尚未就绪的时刻。key会在实体身份变化时重新挂载内层组件保证播种始终诚实。仓库中的迁移印证当前 EditMonitorPage 的三段式结构有趣的是Langfuse 当前的 EditMonitorPage.tsx 已经按此形态迁移完毕可以作为对照样本export default function EditMonitorPage() { return ( MonitorPagePermissions scopealerts:read EditMonitorPageRouter / /MonitorPagePermissions ); } function EditMonitorPageRouter() { // ... const { data, error, isPending } api.monitors.get.useQuery( { projectId, id: monitorId }, { enabled: Boolean(monitorId) }, ); if (isPending) return EditMonitorLoadingPage projectId{projectId} /; if (error) return GetMonitorErrorPage error{error} /; return EditMonitorFormPage monitor{data} /; } const EditMonitorFormPage ({ monitor }: { monitor: Monitor }) { const [liveName, setLiveName] useState(monitor.name); // ... };注意其中的细节都呼应了准则最外层用MonitorPagePermissionsRBAC 权限门控包住被拦截的用户根本不会触发 monitor 查询路由层EditMonitorPageRouter用isPending渲染专属加载页、用error渲染错误页NOT_FOUND时提示Alert not found仅在成功时渲染表单页表单页EditMonitorFormPage用useState(monitor.name)播种liveName整个文件里没有任何 useEffect——state 由 prop 播种由onNameChange处理器更新。这正是文档所倡导的query → 准备好的数据 → 渲染以就绪为门槛的落地形态。从服务器状态派生客户端状态当客户端状态引用服务器数据时——选中的行、当前激活的 id、已选择的选项——只存储用户的意图id在渲染期通过合并查询数据来派生有效值const selectedId useFeatureStore((s) s.selectedId); const selected rows.find((r) r.id selectedId) ?? null;不要写一个在 refetch 时校验或复制服务器数据到客户端 store的 effect。派生deriving保证了每个事实只有一个真相源store 拥有意图查询拥有数据合并操作在加载、refetch、失效invalidation全过程中保持正确。该模式在社区中被称为 Deriving Client State from Server State本文仅转述其思想不涉及外部链接。useMemo只在合并可被测量为昂贵时才包裹——先测量再优化。表单只在初始值已就绪处定义表单表单只能定义在所有初始值都已准备好的位置。如果数据仍在加载表单就要放到组件树的更深层DataPreparerAndController负责拉取、展示加载态纯FormComponent只渲染字段并提交接收initialValues与onSubmit。真实反模式CreateOrEditLLMToolDialog 的 form.reset effect文档引用的第二个真实案例位于 CreateOrEditLLMToolDialog.tsx当前仓库中的代码完全吻合描述const form useForm({ resolver: zodResolver(formSchema), defaultValues: props.defaultValues ?? { /* create 模式默认值 */ }, }); // Populate form when in edit mode useEffect(() { if (existingLlmTool !props.defaultValues) { form.reset({ name: existingLlmTool.name, description: existingLlmTool.description, parameters: JSON.stringify(existingLlmTool.parameters, null, 2), }); } }, [existingLlmTool, form, props.defaultValues]);对话框顶部用 create 模式默认值创建useForm随后靠一个编辑模式时填充表单的 effect 在既有工具可用后调用form.reset(...)。修复是结构性的而不是写一个更好的 effect在渲染表单组件之前决定defaultValuescreate 还是 edit只有当这些值就绪时才渲染表单reseteffect 随之消失。遵循此结构CreateOrEditLLMToolDialog也可以改造成外层控制器拿到existingLlmTool后直接把它作为defaultValues传给内层纯表单组件从而彻底删除第 88–97 行的 effect。大动作Big Actions应生活在 React 之外queryClientReact Query与 vanilla Zustand 的 store 实例在组件树之外也是可达的——没有任何东西强制工作流必须经过 hook 或 effect。在上述同一个对话框中prettifyJson和handleDelete被困在组件内部因为它们闭包捕获了表单 state 和 mutation hooks。重构的方向是把依赖store或一个小的传入依赖对象外置动作就变成普通的、可测试的函数。配套的动作形态定义在 big-feature-rules.mdEffects And Actions 一节与 local-feature-state.mdIndependent Actions 一节中。后者的核心约束是动作不得调用 React hooks通过参数传入 hook 结果、查询辅助函数、本地 store 实例或窄回调依赖需要大量数据准备的在动作旁导出纯 helper让变换可以被独立测试await exportFeatureData({ capture, fetchDetails, projectId, refetchSummary, selectedIds, });同时避免两种极端不要把二十个 props 穿成串只为了让一个按钮能做功能级工作也不要把复杂工作流内联在页面控制器里仅仅因为所有依赖恰好都在作用域内。复杂动作应放在actions/*.ts或命名 store action 中组件负责接线 hooks 并传入依赖动作拥有工作流。什么时候 useEffect 是合法的useEffect的合法用途是触及 React 渲染之外存在的东西DOM 与浏览器 APIdocument.addEventListener/removeEventListenerobservers观察器命令式第三方 APIWeb Audio它还额外要求用户手势。一个健康的 effect 具备三个特征没有依赖或只有最少且稳定的依赖单一职责一个 concern有 cleanup 函数。反过来说如果某个 effect 在反复写状态那么应该让对应的 store action 幂等idempotent而不是容忍这个 effect。useCallback 与 useMemo 是过早优化useCallback本质上就是函数的useMemo。在确认并测量出性能问题之前不要 memoize。正确的性能修法是拆分组件修正重渲染边界——拆分本身就是一种 memoization处处都需要 memoization意味着组件重渲染过于频繁意味着状态边界放错了。去修边界而不是到处加缓存。唯一的例外是把引用稳定性当作正确性需求当某个回调或对象是一个合法的集成 effect 或数据拉取 hook 的依赖时memoize 它——或者把它提升hoist到组件外部——避免依赖每次渲染都改变身份从而让 effect 循环触发。这是正确性不是优化。优先 UI 方案而非代码方案很多前端问题有UI 方案而不是代码方案加载状态 门控渲染删除的复杂度超过任何行为保持的巧妙重构不要害怕改变行为来简化在页面原本显示半成品表单的地方渲染一个骨架屏是改进不是回退。这同时也解释了为什么先写表征测试characterization test再保持行为地重构很少适用于大型前端重构——目标行为往往就是更简单的那个而不是当前的那个。愿景数据的单向流动架构的终态清晰而简洁拉取的数据单向流动query → 准备好的数据 → 渲染以就绪为门槛gated on readiness状态由 props 播种、由事件处理器编辑绝不被 effect 镜像动作是组件树之外的普通函数残存的 effect 是带有 cleanup 的薄薄一层 DOM/浏览器集成。Langfuse 前端web/src仍有数百处不符合此形态的代码——追踪traces、观察observations、实验experiments、提示词prompts、评测evals、数据集datasets、会话sessions等页面都存在控制器过重的表面。当你触碰其中一个时朝着这个形态去迁移它而不是继续扩展 effect完整的迁移路径参考 controller-migration.md本地状态与 store 的形状参考 local-feature-state.md全套硬性规则见 big-feature-rules.md。【免费下载链接】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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价