OpenMontage React 最佳实践:渲染期计算派生状态——停止用 useEffect 同步 state,直接从 props/state 派生
2026/9/9 23:48:13 网站建设 项目流程

OpenMontage React 最佳实践:渲染期计算派生状态——停止用 useEffect 同步 state,直接从 props/state 派生

【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage

本文为 OpenMontage 技能库vercel-react-best-practices中第 5 章「Re-render Optimization(重渲染优化)」的首条规则Calculate Derived State During Rendering(渲染期计算派生状态)的完整解读。OpenMontage 作为 agentic 视频生产系统,在.agents/skills/下维护了 700 余个面向 AI 编码助手的「生产知识文件」,本文档(rerender-derived-state-no-effect.md)就是其中用于指导 React/Next.js 代码生成与评审的规则之一。读完后,你将掌握:该规则在技能体系中的位置与编译流程、用 effect 同步派生值为何必然引入多余渲染与状态漂移、以及如何在实际组件(包括本仓库 Remotion composer 代码)中正确地在渲染期派生状态。

规则在技能体系中的位置

该规则文件位于 .agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md,其 YAML frontmatter 定义了规则在技能库中的全部元数据:

--- title: Calculate Derived State During Rendering impact: MEDIUM impactDescription: avoids redundant renders and state drift tags: rerender, derived-state, useEffect, state ---

这几个字段并非摆设,它们直接驱动技能库的构建与检索:

  • 文件名前缀决定所属章节:按 README.md 的命名约定,rerender-前缀对应第 5 章「Re-render Optimization」。各章节的前缀、影响级别与描述统一定义在 rules/_sections.md 中——第 5 章标注为MEDIUM级影响,目标是「减少不必要的重渲染,避免浪费计算、提升 UI 响应性」。
  • 章节内按标题字母序自动编号:构建时会把所有规则按 title 排序并自动生成 ID。该规则在编译产物 AGENTS.md 中被编译为5.1 Calculate Derived State During Rendering(「5. Re-render Optimization」章节的第一条)。
  • 影响级别语义impact: MEDIUM对应 README 中定义的中档收益(「Moderate performance improvements」),impactDescription(avoids redundant renders and state drift)则说明收益来源是消除冗余渲染和状态漂移。
  • tags 用于检索rerender, derived-state, useEffect, state四个标签让 Agent 在按主题检索规则时能快速命中。

整个技能由 SKILL.md 作为可加载入口:它声明了 65 条规则、8 个类别,并明确该技能「应在编写、评审或重构 React/Next.js 代码,确保最优性能模式时」被触发;本规则在其中被登记为:

rerender-derived-state-no-effect- Derive state during render, not effects

规则文件本身遵循 rules/_template.md 定义的结构:一句话原则 →Incorrect(错误示例)Correct(正确示例)→ 补充说明与引用。技能库支持pnpm build(将规则编译为 AGENTS.md)、pnpm validate(校验规则文件)与pnpm extract-tests(抽取 LLM 评估用例)三个脚本;在本仓库中只需查看编译后的 AGENTS.md 即可获得全部规则的长文形式。

核心原则:能在渲染期算出来的,就不要存进 state

规则的完整表述只有两句话,但覆盖面很广:

If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.

即:凡是能由当前 props/state 计算出来的值,都不要存进 state,也不要用 effect 去更新它;直接在渲染期派生,以避免多余渲染与状态漂移。也不要在 effect 中仅仅响应 prop 变化就 setState,应优先用派生值或 keyed reset(用 key 强制重置)。

这里点出了两个独立的反模式:

  1. 派生值存 state:把fullName这类纯计算结果提升为独立状态;
  2. effect 响应 propuseEffect的依赖里只有 prop/state,效果是setX(prop 的函数)——这是 React 官方文档所称「You Might Not Need an Effect」的典型场景,也是该规则文末引用的参考主题。

为什么错误写法会「双渲染」且「漂移」

规则给出的反例代码:

function Form() { const [firstName, setFirstName] = useState('First') const [lastName, setLastName] = useState('Last') const [fullName, setFullName] = useState('') useEffect(() => { setFullName(firstName + ' ' + lastName) }, [firstName, lastName]) return <p>{fullName}</p> }

对照 React 的「render → commit → 运行 effect」生命周期,一次firstName变更的实际代价是:

  1. 第一次渲染firstName已更新,但fullName仍是旧值(effect 还没跑)——用户会短暂看到过期内容;
  2. effect 在提交后触发setFullName(...)安排第二次渲染,此时fullName才追上。

也就是说,每次 prop 变化都付出两次渲染,且中间帧展示的永远是滞后一拍的派生值。初始状态下问题更直观:fullName初始为'',首屏直接渲染一个空字符串,直到挂载后的 effect 把它补上——这就是规则标题里的state drift(状态漂移)fullName作为一个独立状态源,与它「本该等于」的firstName + ' ' + lastName在时间轴上永久差一个 effect 的时延,任何 prop 的快速连续变更、StrictMode 下的双调用、或多个此类 effect 的先后顺序,都会放大这种不一致。

正确写法则只有五行核心逻辑:

function Form() { const [firstName, setFirstName] = useState('First') const [lastName, setLastName] = useState('Last') const fullName = firstName + ' ' + lastName return <p>{fullName}</p> }

fullName不再拥有独立生命周期,它每次渲染都从当前 props/state 即时算出,渲染次数减半、漂移在结构上不可能发生,代码还少了一个状态、一个 setter 和一个 effect 的维护面。

判定清单:什么时候该派生,什么时候必须用 state

规则给出的判据是「能否由当前 props/state 计算出来」。落到日常代码,可以按下面的清单自测:

场景结论
由现有 props/state 纯函数可得(拼接、格式化、过滤、区间计算)渲染期直接派生
effect 里唯一的逻辑就是setX(基于依赖的表达式)几乎总是应该改为渲染期派生
用户输入、累积历史、一次性外部数据(请求结果、订阅源)合法 state,不在本规则管辖内
prop 变化后需要把子组件的内部状态「归零」key={...}强制 remount(规则所称 keyed reset),而不是在 effect 里 setState
派生计算本身昂贵仍属于派生值,应配合同章节的 memo/useMemo 类规则(如rerender-memo)缓存,而不是退回「state + effect」

其中keyed reset值得单独强调:很多人写useEffect(() => setDraft(props.seed), [props.seed])是想实现「seed 变了草稿就重置」。规则给出的正解是给子组件传key={props.seed},让 React 以全新挂载代替状态修补——既避免 effect 时序问题,也天然清掉子树下所有内部状态。

仓库实例:Remotion composer 中的时间轴派生

本仓库的视频合成层remotion-composer/是一个 React + TypeScript 代码库,恰好大量实践了本规则。以 EndTag.tsx 为例,它是片尾卡组件,所有时间轴节点都是在渲染函数体内从 props 直接派生的:

const frame = useCurrentFrame() const { fps } = useVideoConfig() // Timing in frames const fadeInFrames = Math.round(fadeInSeconds * fps) const holdFrames = Math.round(holdSeconds * fps) const fadeOutFrames = Math.round(fadeOutSeconds * fps) // Full opacity envelope: fade in -> hold -> fade out const fadeInEnd = fadeInFrames const fadeOutStart = fadeInEnd + holdFrames const fadeOutEnd = fadeOutStart + fadeOutFrames

fadeInFramesfadeOutStartfadeOutEnd完全由 props(fadeInSeconds等)与视频配置(fps)决定,组件没有为它们建任何 state,也没有任何 effect 去「同步」——这正是规则 Correct 示例的规模化版本。同文件里下划线动画的underlineStartFrame = fadeInEnd + Math.round(0.2 * fps)也是从派生值再派生,链条依然全部落在渲染期。类似地,AnimeScene.tsx 中的const imageCount = images.length也是渲染期直接从 props 派生的计数。从源码结构看,Remotion 组件的每帧渲染是逐帧重算的过程,「无 effect、无 state 同步、全部派生」的写法在这里不仅是性能优化,更是保证帧输出确定性的正确性手段——这解释了该仓库在把 agent 生成/评审 React 组件代码时引入这套 Vercel 规则库的现实背景。

与同章节规则的关系

本规则不是孤立的一条,它是rerender-前缀 15 条规则组成的体系中的一环(见 SKILL.md 第 5 节清单)。与它最贴近的两条:

  • rerender-derived-state.md(Subscribe to Derived State):讲的是订阅层面——订阅派生后的布尔值(如useMediaQuery('(max-width: 767px)')得到的isMobile)而不是连续原始值(如窗口宽度),让重渲染只在布尔翻转时发生。一条管「渲染期怎么算」,一条管「订阅什么值」,方向互补。
  • rerender-memo.md 等缓存类规则:当派生计算昂贵时,用记忆化而不是状态同步来降频。

三条合在一起构成完整的派生值治理:渲染期派生为默认,昂贵计算加 memo,订阅收窄到布尔

小结

Calculate Derived State During Rendering这条 MEDIUM 级规则用最小篇幅钉死了一条高频反模式:「value = f(props, state)」的结果不是状态,不该有useState+useEffect的同步链路。遵循它,每次 prop 变更的渲染次数从两次降为一次,状态漂移被结构消除;遇到「prop 变化重置内部状态」的需求,改用 keyed reset。在 OpenMontage 这类以 AI 编码助手为核心的生产系统里,这条以固定模板(frontmatter + Incorrect/Correct 对照 + 参考)编写的规则文件可以被pnpm build编译进 AGENTS.md 供 Agent 在生成与评审 React 代码时逐条执行,也让本仓库 remotion-composer/src/components/ 下「渲染期全派生」的组件写法有了明确的规范出处。

【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询