- 人工智能
- 大模型
- AI 应用
- 前端
- 后端
【免费下载链接】BibiGPT-v1
BibiGPT v1 · one-Click AI Summary for Audio/Video & Chat with Learning Content: Bilibili | YouTube | Tweet丨TikTok丨Dropbox丨Google Drive丨Local files | Websites丨Podcasts | Meetings | Lectures, etc. 音视频内容 AI 一键总结 & 对话:哔哩哔哩丨YouTube丨推特丨小红书丨抖音丨快手丨百度网盘丨阿里云盘丨网页丨播客丨会议丨本地文件等 (原 BiliGPT 省流神器 & AI课代表)
导读
在 React 组件中,如果一个值可以由当前的 props 或 state 直接计算得出,就不要把它存进 state,更不要用useEffect去“同步”它——正确的做法是在渲染期间直接派生(derive)该值。这篇指南以 Vercel React Best Practices 规则 rerender-derived-state-no-effect.md 为骨架,完整讲解这条规则的错误/正确写法、背后的渲染原理,并结合 BibiGPT-v1 仓库中的真实源码(窗口尺寸、总结结果格式化、笔记按钮可见性等场景)展示如何在生产级 Next.js 应用中落地。读完你将掌握:识别“冗余 state + effect”反模式、用渲染期派生重构、区分派生值与真正副作用,从而减少组件不必要的重渲染并消除状态漂移。
一、规则定义:渲染期间计算派生状态(Calculate Derived State During Rendering)
该规则位于仓库 .agents/skills/vercel-react-best-practices 的第 5 类“Re-render Optimization(重渲染优化,MEDIUM 影响级别)”中,属于rerender-前缀规则族。规则的完整表述是:
如果一个值可以由当前的 props/state 计算得出,就不要把它存储在 state 中,也不要在 effect 中更新它。在渲染期间派生它,以避免多余的渲染和状态漂移。不要仅仅为了响应 props 变化而在 effect 中设置 state;应优先使用派生值或 keyed reset(通过
key重置组件)替代。
规则元信息(frontmatter):
| 字段 | 值 | 含义 |
|---|---|---|
title | Calculate Derived State During Rendering | 规则标题 |
impact | MEDIUM | 影响级别:中等(对应 SKILL.md 中的优先级 5,Re-render Optimization) |
impactDescription | avoids redundant renders and state drift | 效果:避免冗余渲染与状态漂移 |
tags | rerender, derived-state, useEffect, state | 检索标签 |
一句话概括:props/state 能算出来的,别存 state;state 能算出来的,别用 effect 同步。
二、反例剖析:冗余 state + effect 的经典错误写法
原规则给出了一个极其典型的反例——表单组件中维护fullName副本:
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> }这段代码存在三个层次的问题:
- 多余的 state:
fullName完全由firstName与lastName推导而来,却在组件里单独占据一份 state; - 多余的 effect:
useEffect仅为了“把 A 的值抄给 B”,制造了一次额外的渲染周期。React 的渲染流程是 render → commit → effect,effect 里setFullName又会触发下一次 render,等于每次输入变化都白跑了两轮 render; - 状态漂移(state drift)风险:
fullName与firstName/lastName是两份独立状态,一旦某个路径(比如后续新增代码、持久化恢复)只更新了其中一部分,二者就会不一致,出现“数据幽灵”。规则 frontmatter 中的impactDescription: avoids redundant renders and state drift正是针对后两点。
三、正例:把派生逻辑放进渲染函数
正确的写法是删除 state 与 effect,直接在渲染期间计算:
function Form() { const [firstName, setFirstName] = useState('First') const [lastName, setLastName] = useState('Last') const fullName = firstName + ' ' + lastName return <p>{fullName}</p> }关键差异:
fullName不再是 state,而是一个普通的局部常量;- 每次渲染时它都会基于最新的
firstName/lastName重新计算,天然同步,不存在漂移; - 输入变化只触发一次渲染,没有 effect 引发的第二次渲染;
- 组件卸载时无需清理任何东西,少写代码、少犯错。
这种“纯计算”还有一个额外收益:因为派生逻辑是纯函数式的,它可以在任何时候被安全地useMemo包裹(当计算代价高时),而useEffect里的 setState 模式则无法这样简单优化。注意,规则族的姊妹篇 rerender-simple-expression-in-memo.md 提醒:对firstName + ' ' + lastName这类简单表达式,连useMemo都不需要,直接裸算即可。
四、深入原理:为什么“渲染期派生”能同时消除冗余渲染与状态漂移
4.1 渲染循环视角
React 的组件更新可概括为:props/state 变化 → render(重新执行组件函数)→ commit DOM。如果把派生值放进 state + effect:
输入变化 → render#1 → commit → effect 执行 setFullName → render#2 → commit多出一整轮“render + commit”。在频繁输入(如搜索框、字幕时间戳开关)或组件树较大的页面上,这种多余的渲染会被成倍放大。而渲染期派生把计算直接放进 render 阶段:
输入变化 → render(内部同步算出 fullName)→ commit严格一次渲染,性能与心智模型都更优。
4.2 状态一致性视角
两份 state 之间靠 effect “事后补齐”,本质上是一种弱一致模型:在firstName变化后、effect 尚未运行前的间隙,fullName依然是旧值。依赖多个输入时这种不一致更容易被触发(例如 props 在 effect 依赖数组中未列全导致的陈旧值)。派生值则是在 render 的同一时刻、由同一份最新输入计算,数学上不可能出现不一致。
4.3 与 React 官方指导的关系
规则末尾引用了 React 官方文档《You Might Not Need an Effect》(你可能不需要 Effect)。官方给出的三类“不需要 Effect”的场景,本条规则对应的是第一类——根据 props 或 state 调整其他状态。官方同样推荐:基于现有 props/state 计算的值,直接在渲染期间计算;同时官方明确反对在 effect 中“链式”更新 state(如setFullName),因为这会导致额外的渲染周期。
五、仓库源码佐证:BibiGPT-v1 中的派生状态实践
BibiGPT-v1 是一个 Next.js + React 18 的音视频 AI 总结应用(package.json中react: "18.2.0"、next: "^16.1.6")。其源码中大量采用了“渲染期派生”模式,可以作为这条规则的生产级参考。
5.1 窗口尺寸与移动端判断:派生布尔值
hooks/use-window-size.ts 在 effect 中只订阅真实的外部事件(resize),然后在渲染期派生出isMobile/isDesktop:
export default function useWindowSize() { const [windowSize, setWindowSize] = useState<{ width: number | undefined height: number | undefined }>({ width: undefined, height: undefined, }) useEffect(() => { function handleResize() { setWindowSize({ width: window.innerWidth, height: window.innerHeight, }) } window.addEventListener('resize', handleResize) handleResize() return () => window.removeEventListener('resize', handleResize) }, []) return { windowSize, isMobile: typeof windowSize?.width === 'number' && windowSize?.width < 768, isDesktop: typeof windowSize?.width === 'number' && windowSize?.width >= 768, } }注意这里的边界把握:
windowSize是必须的 state:它来自外部浏览器环境,无法从 props/state 推导,必须用 effect 订阅resize事件——这是 effect 的正确用途;isMobile/isDesktop是派生值:由windowSize.width在渲染时直接算出,没有为它们单独建 state,也没有用 effect 同步。这一写法同时呼应了姊妹规则 rerender-derived-state.md 的进阶建议:当只需要“是否移动端”这个布尔结果时,可进一步用useMediaQuery订阅布尔值而非连续数值,减少重渲染频率。
5.2 总结结果格式化:多层渲染期派生
components/SummaryResult.tsx 展示了更复杂的派生链——从原始summary字符串出发,在渲染函数体内连续派生多个值,完全没有 effect:
export function SummaryResult({ currentVideoUrl, currentVideoId, summary, shouldShowTimestamp }) { const { toast } = useToast() const formattedCachedSummary = summary?.startsWith('"') ? summary.substring(1, summary.length - 1).split('\\n').join('\n') : summary const { summaryArray, formattedSummary } = formatSummary(formattedCachedSummary) const summaryNote = formattedSummary + '\n\n#BibiGPT https://b.jimmylv.cn @吕立青_JimmyLv \nBV1fX4y1Q7Ux' // ... }这里的派生逻辑有:
formattedCachedSummary:清洗缓存 JSON 字符串的引号与转义换行,由summary派生;summaryArray/formattedSummary:调用 utils/formatSummary.ts 的formatSummary()纯函数,把总结文本按\n-拆分成带时间戳的条目数组并重组文本,全部在渲染期完成;summaryNote:在formattedSummary基础上拼接落款。
值得强调的是formatSummary本身是一个模块级纯函数(输入字符串、输出{ summaryArray, formattedSummary }),这意味着派生逻辑与组件解耦、易于单测,这正是渲染期派生模式对代码结构带来的正面影响。SummaryResult只是消费其结果,没有为“格式化后的文本”维护任何副本 state。
5.3 聊天状态派生:loading 与 summary
hooks/useSummarize.ts 使用@ai-sdk/react的useChat管理消息流,并在此基础上派生上层状态:
const loading = status === 'submitted' || status === 'streaming' const summary = useMemo(() => extractLatestAssistantText(messages), [messages])loading由status直接派生(布尔表达式裸算,简单表达式无需 memo);summary从messages派生,且因为extractLatestAssistantText涉及数组遍历(reverse+find+filter),用useMemo缓存以避免每次渲染重复遍历——这体现了 5.2 节提到的“计算代价较高时再 memo”的平衡。
同时该文件的useEffect只保留给真正的副作用:监听error变化弹出 toast(错误提示是外部副作用,无法在渲染期完成),并在501/504/401等状态码下分别给出差异化提示。对照 rerender-move-effect-to-event.md 的视角,BibiGPT 把用户主动触发的“提交总结”动作(summarize函数)放在事件处理器里直接调用setMessages([])与sendMessage,而不是建模成“submitted state + effect”,同样遵循了“副作用进事件处理器”的相邻规则。
5.4 笔记按钮可见性:props/state 组合派生
components/ActionsAfterResult.tsx 在渲染期间组合两个 localStorage 值(flomo 与飞书 webhook)派生按钮状态:
const [flomoWebhook] = useLocalStorage<string>('user-flomo-webhook') const [larkWebhook] = useLocalStorage<string>('user-lark-webhook') const hasNoteSetting = flomoWebhook || larkWebhookhasNoteSetting是渲染期派生的布尔值,直接驱动{!hasNoteSetting ? <Link> : <>...</>}的条件渲染。若改用“effect 监听 webhook 变化再 setState 一个 hasNoteSetting”,不仅多一轮渲染,还会在初始化、清空 webhook 等边界场景出现按钮状态滞后。
5.5 初始化同步与 localStorage:伴生的正确模式
再看与派生状态配套的初始化与持久化模式。仓库的 hooks/useLocalStorage.ts 遵循 rerender-lazy-state-init.md 规则,把初始化逻辑作为函数传给useState,使其只在首次渲染执行一次(并优雅处理 SSR 与解析错误):
const [storedValue, setStoredValue] = React.useState<T>(() => { if (typeof window === 'undefined') { return initialValue } try { const item = window.localStorage.getItem(key) return item ? JSON.parse(item) : initialValue } catch (error) { console.error(error) return initialValue } })这正是“能够一次算出的值就不要反复用 effect 同步”思路的镜像:该 effect 的交给 effect(订阅 resize、上报 analytics),该渲染期计算的就在渲染期计算,该初始化的就惰性初始化一次。
5.6 一个对照:什么时候 effect 是必要的
同样在仓库 pages/index.tsx 中,useEffect被用于加载 Segment analytics 并执行页面上报:
useEffect(() => { async function handleAnalyticsLoading(browser: AnalyticsBrowser) { try { const [response, ctx] = await browser setAnalytics(response) ;(window as any).analytics = response ;(window as any).analytics?.page() } catch (err) { console.error(err) setAnalytics(undefined) } } handleAnalyticsLoading(analyticsBrowser).catch(console.error) }, [analyticsBrowser])这是正确的 effect 用法:异步加载第三方 SDK 并写入window,属于“连接到外部系统”的副作用,无法通过渲染期计算替代。规则的边界正在于此——渲染期派生只适用于“纯计算”,凡涉及订阅外部事件、网络请求、写入 DOM/全局对象等副作用,仍应留在 effect 或事件处理器中。
六、何时用 keyed reset 替代“props 变化重置 state”
规则中特别提到“prefer derived valuesor keyed resets”。当派生值无法覆盖某个场景时——例如一个表单组件需要在userId(props)变化时把输入历史整体清空,而不仅仅是重算某个值——正确做法不是写 effect 监听userId再setState,而是给组件一个key:
function ProfilePage({ userId }) { // key 变化 → 整个组件重新挂载 → 内部 state 自然重置 return <ProfileForm key={userId} userId={userId} /> } function ProfileForm({ userId }) { const [draft, setDraft] = useState('') // 不需要 useEffect(() => setDraft(''), [userId]) // ... }keyed reset利用了 React 的挂载机制:key改变时组件卸载并重新创建,内部 state 天然回到初始值,无需 effect、无需手动清空、不会出现“旧 state 闪现”的中间态。判断口诀:
- 要实时重算的值 → 渲染期派生;
- 要整体重置的一组 state →
key重置; - 要订阅外部系统(事件、请求、定时器)→ effect;
- 要响应用户动作的副作用 → 事件处理器。
七、快速自查清单
把规则转化为可操作的代码评审清单,在写或改 React 组件时逐条过一遍:
- 这个值能从现有 props/state 计算出来吗?→ 是:渲染期裸算或
useMemo,绝不存 state; - 这个 effect 是否只是在“把值从 A 抄到 B”?→ 是:删除 effect 与副本 state;
- 是否在 effect 里 setState 仅为了响应 props 变化?→ 是:改为派生值或给组件加
key; - 派生计算成本高吗?→ 高:
useMemo包裹并确保依赖数组完整;低:直接裸算; - 真正的外部副作用(订阅、请求、上报)?→ 保留 effect,或按 rerender-move-effect-to-event.md 移入事件处理器;
- 初始化成本高或依赖外部存储?→ 参考 hooks/useLocalStorage.ts 的惰性
useState(() => ...)初始化模式。
八、结语
“Calculate Derived State During Rendering”是 React 性能优化中最基础也最容易被忽略的一条:它不需要 memo、不需要复杂的调度,只需要你克制地使用 state 与 effect。在 BibiGPT-v1 这类交互密集的 Next.js 应用中,窗口尺寸判断、总结文本格式化、按钮可见性等场景全部采用渲染期派生,配合“effect 只留给真正副作用”的纪律(use-window-size.ts、useSummarize.ts),从根上避免了冗余渲染和状态漂移。这条规则与同族的 rerender-derived-state.md、rerender-lazy-state-init.md、rerender-move-effect-to-event.md 共同构成一套“能不算则不存、能渲染期则不进 effect”的完整方法论,值得作为 React 代码评审的默认检查项。
- 人工智能
- 大模型
- AI 应用
- 前端
- 后端
【免费下载链接】BibiGPT-v1
BibiGPT v1 · one-Click AI Summary for Audio/Video & Chat with Learning Content: Bilibili | YouTube | Tweet丨TikTok丨Dropbox丨Google Drive丨Local files | Websites丨Podcasts | Meetings | Lectures, etc. 音视频内容 AI 一键总结 & 对话:哔哩哔哩丨YouTube丨推特丨小红书丨抖音丨快手丨百度网盘丨阿里云盘丨网页丨播客丨会议丨本地文件等 (原 BiliGPT 省流神器 & AI课代表)
相关推荐
React 渲染期派生状态计算指南:用「渲染时计算」替代「useEffect 同步」,消除冗余重渲染与状态漂移
React 渲染期派生状态计算指南:用「渲染时计算」替代「useEffect 同步」,消除冗余重渲染与状态漂移 导读 在 React 组件中,当某个值可以由当前
人工智能AI Agent代码智能体Agent 编排CLIAI 应用JetBrains CC GUI 前端实践指南:渲染期派生状态替代 useEffect 同步,根治冗余重渲染与状态漂移
JetBrains CC GUI 前端实践指南:渲染期派生状态替代 useEffect 同步,根治冗余重渲染与状态漂移 导读 本文围绕 .agents/skil
YOLO Tracking + Keypoint R-CNN 实战:如何搭一个 ID 不跳变的实时人体姿态跟踪系统
YOLO Tracking + Keypoint R CNN 实战:如何搭一个 ID 不跳变的实时人体姿态跟踪系统 纯检测模型逐帧出框,同一行人走到下一帧就"换
人工智能计算机视觉深度学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考