ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Plate 前端工程实践:React useEffect 九大反模式与替代方案详解

Plate 前端工程实践:React useEffect 九大反模式与替代方案详解 Plate 前端工程实践React useEffect 九大反模式与替代方案详解【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate本文基于 Plate基于 Slate 的富文本编辑器项目集成 AI 与 shadcn/ui仓库中维护的 Agent 技能文档 anti-patterns.md 展开系统讲解useEffect的九种典型误用、每种反模式对应的正确写法并结合 SKILL.md 的决策树与 alternatives.md 的八种替代模式帮助读者建立先判断是否需要 Effect、再决定写不写 Effect的 React 状态管理心智模型同时给出 Plate 源码中useSyncExternalStore的真实工程示例。文档背景与核心宗旨在 Plate 仓库的.agents/skills/目录中react-useeffect是一套供 AI Agent 在编写或审查 React 代码时引用的技能文档由三个文件组成SKILL.md快速对照表、决策树与适用场景说明anti-patterns.md九个反模式的 BAD/GOOD 对照代码本文主体alternatives.md八种替代方案的完整实现示例。整套文档的第一性原理只有一句话见 SKILL.mdEffect 是 React 的逃生舱口escape hatch它让你与外部系统同步。如果不涉及外部系统你就不应该需要 Effect。围绕这一宗旨SKILL.md 给出了一张快速参考表覆盖六类最常见的场景场景不要做应该做从 props/state 派生状态useStateuseEffect渲染时直接计算昂贵计算useEffect做缓存useMemoprop 变化时重置状态useEffect里setStatekeyprop响应用户事件useEffect监听状态事件处理器通知父组件变更useEffect调用onChange在事件处理器中调用数据请求无 cleanup 的useEffect带 cleanup 的useEffect或框架机制以及一棵判断该用什么的决策树需要响应某件事 ├── 用户交互点击、提交、拖拽 │ └── 用事件处理器 ├── 组件出现在屏幕上 │ └── 用 Effect外部同步、埋点等 ├── props/state 变化且需要派生值 │ └── 渲染期间计算 │ └── 昂贵用 useMemo └── prop 变化时需要重置状态 └── 在组件上用 key prop九大反模式逐一拆解以下九个反模式完整继承自 anti-patterns.md每个都给出错误写法、正确写法及其背后原理。1. 用冗余 State 存储派生值最经典的反模式把可以从已有 state/props 计算出来的值再存一份 state并用 Effect 保持同步。// BAD: 多余的 state Effect 同步派生值 function Form() { const [firstName, setFirstName] useState(Taylor); const [lastName, setLastName] useState(Swift); const [fullName, setFullName] useState(); useEffect(() { setFullName(firstName lastName); }, [firstName, lastName]); } // GOOD: 渲染期间直接计算 function Form() { const [firstName, setFirstName] useState(Taylor); const [lastName, setLastName] useState(Swift); const fullName firstName lastName; // 直接算 }为什么坏Effect 方式会引入一次带着旧值渲染、再因 state 更新二次渲染的多余渲染路径。首次渲染时fullName还是空字符串Effect 运行后才更新为正确值——组件会经历一次显示过期值、一次显示新值的两个渲染过程。而渲染期间计算没有这个问题值永远和当前firstName/lastName同步零额外渲染。这也是 README.md 中Stale Data短暂显示旧值故障模式的直接成因与修复方案。2. 在 Effect 中过滤/转换数据与上一条同源把为渲染准备数据这件事放进 Effect同样会多一次渲染并暴露中间态。// BAD: Effect 里过滤列表 function TodoList({ todos, filter }) { const [visibleTodos, setVisibleTodos] useState([]); useEffect(() { setVisibleTodos(getFilteredTodos(todos, filter)); }, [todos, filter]); } // GOOD: 渲染期间过滤昂贵则用 useMemo 缓存 function TodoList({ todos, filter }) { const visibleTodos useMemo( () getFilteredTodos(todos, filter), [todos, filter] ); }对是否昂贵的量化判断alternatives.md 给了一个实用的经验值用console.time/console.timeEnd测一次过滤耗时超过 1ms 才考虑useMemo。同时它补充了一个面向未来的注脚React Compiler 可以自动完成记忆化未来手动useMemo的需求会减少。3. prop 变化时用 Effect 重置状态// BAD: Effect 里重置状态 function ProfilePage({ userId }) { const [comment, setComment] useState(); useEffect(() { setComment(); }, [userId]); } // GOOD: 用 key prop function ProfilePage({ userId }) { return Profile userId{userId} key{userId} /; } function Profile({ userId }) { const [comment, setComment] useState(); // 自动重置 }key 为什么有效React 把 key 不同的组件视为不同组件key 变化即销毁旧实例、重建新实例所有 state 从头开始无需任何 Effect 参与。alternatives.md 强调 key 的适用场景是身份属性变化时希望整个组件全新开始——比如Profile里同时还有likes等多个 statekey 会一次性全部重置而 Effect 方案往往要一条条手动setState极易遗漏。4. 在 Effect 里执行事件专属逻辑区分因为组件渲染而该发生的事和因为某个用户动作而该发生的事是 Effect 使用边界的关键。// BAD: 在 Effect 里响应加入购物车的结果 function ProductPage({ product, addToCart }) { useEffect(() { if (product.isInCart) { showNotification(Added ${product.name}!); } }, [product]); function handleBuyClick() { addToCart(product); } } // GOOD: 在事件处理器里完成 function ProductPage({ product, addToCart }) { function handleBuyClick() { addToCart(product); showNotification(Added ${product.name}!); } }为什么坏Effect 只知道状态变了不知道为什么变。页面刷新时isInCart本来就是trueEffect 照样触发用户会在没做任何操作时看到一个已加入通知。而事件处理器明确知道这次是因为点击购买语义精确、时机可控。alternatives.md 还给出了多入口共享逻辑的补充模式把buyProduct()抽成普通函数handleBuyClick与handleCheckoutClick各自在其后追加差异逻辑如跳转结账页避免用 Effect 去猜用户意图。5. Effect 链条Effect Chains// BAD: Effect 连环触发 function Game() { const [card, setCard] useState(null); const [goldCardCount, setGoldCardCount] useState(0); const [round, setRound] useState(1); const [isGameOver, setIsGameOver] useState(false); useEffect(() { if (card?.gold) setGoldCardCount(c c 1); }, [card]); useEffect(() { if (goldCardCount 3) { setRound(r r 1); setGoldCardCount(0); } }, [goldCardCount]); useEffect(() { if (round 5) setIsGameOver(true); }, [round]); } // GOOD: 在事件处理器中一次性算出所有新状态 function Game() { const [card, setCard] useState(null); const [goldCardCount, setGoldCardCount] useState(0); const [round, setRound] useState(1); const isGameOver round 5; // 派生值 function handlePlaceCard(nextCard) { if (isGameOver) throw Error(Game ended); setCard(nextCard); if (nextCard.gold) { if (goldCardCount 3) { setGoldCardCount(goldCardCount 1); } else { setGoldCardCount(0); setRound(round 1); if (round 5) alert(Good game!); } } } }为什么坏一次放牌操作会引发setCard → setGoldCardCount → setRound → setIsGameOver的连锁反应每个 Effect 都触发新一轮渲染整条链路又慢又难跟文档还特别指出这种结构对历史回放history replay类特性尤其脆弱——你需要重放整条 Effect 触发序列才能复现状态。正确做法是在一个事件处理器内把所有下一步状态算完再批量提交能派生的如isGameOver就渲染期间派生。6. 通过 Effect 通知父组件// BAD: Effect 里调 onChange function Toggle({ onChange }) { const [isOn, setIsOn] useState(false); useEffect(() { onChange(isOn); }, [isOn, onChange]); function handleClick() { setIsOn(!isOn); } } // GOOD: 同一事件里通知 function Toggle({ onChange }) { const [isOn, setIsOn] useState(false); function updateToggle(nextIsOn) { setIsOn(nextIsOn); onChange(nextIsOn); // 同一事件批量渲染 } function handleClick() { updateToggle(!isOn); } } // BEST: 完全受控组件 function Toggle({ isOn, onChange }) { function handleClick() { onChange(!isOn); } }文档给出了三档递进Effect 同步最差→ 半受控组件在同一事件中setStateonChange好→ 完全受控组件最佳。受控组件没有本地 state就不存在两个数据源可能不同步的问题这也与 Plate 这类复杂编辑器组件大量受控的选中态、工具栏状态的工程取向一致。7. 通过 Effect 把数据向上传递// BAD: 子组件取数再用 Effect 传给父组件 function Parent() { const [data, setData] useState(null); return Child onFetched{setData} /; } function Child({ onFetched }) { const data useSomeAPI(); useEffect(() { if (data) onFetched(data); }, [onFetched, data]); } // GOOD: 父组件取数向下传递 function Parent() { const data useSomeAPI(); return Child data{data} /; }原理数据应该向下流动。子组件取数再经 Effect 上抛数据源、缓存与失效逻辑散落在两个组件中调试困难且容易形成隐式依赖。把请求提升到父组件或更合理的共同祖先数据流恢复单向alternatives.md 将此归入State 上提Lifting State Up模式。8. 请求数据但缺少 cleanup竞态条件这是九个反模式中唯一一个Effect 本身是必要的、写法有缺陷的案例也是 README.md 中Race Conditions快速输入时旧查询结果覆盖新结果故障模式的修复点。// BAD: 无 cleanup存在竞态 function SearchResults({ query }) { const [results, setResults] useState([]); useEffect(() { fetchResults(query).then(json { setResults(json); // hello 的响应可能晚于 hell 到达 }); }, [query]); } // GOOD: cleanup 让过期响应失效 function SearchResults({ query }) { const [results, setResults] useState([]); useEffect(() { let ignore false; fetchResults(query).then(json { if (!ignore) setResults(json); }); return () { ignore true; }; }, [query]); }机制说明query每次变化React 会先运行上一次 Effect 返回的 cleanup置ignore true再运行新的 Effect。旧请求的 Promise 稍后 resolve 时闭包里的ignore已为truesetResults被跳过过期响应无法污染 UI。alternatives.md 进一步给出了带loading/error的完整useData自定义 Hook 模板cleanup 中对setData/setError/setLoading全部加ignore守卫并建议优先使用框架级数据获取方案README.md 补充了AbortController作为另一条修复路径。9. 在 Effect 中做应用级初始化// BAD: 开发模式下会跑两次可能破坏鉴权 function App() { useEffect(() { loadDataFromLocalStorage(); checkAuthToken(); // 第二次调用可能使 token 失效 }, []); } // GOOD: 模块级守卫 let didInit false; function App() { useEffect(() { if (!didInit) { didInit true; loadDataFromLocalStorage(); checkAuthToken(); } }, []); } // ALSO GOOD: 提升到模块级执行 if (typeof window ! undefined) { checkAuthToken(); loadDataFromLocalStorage(); }背景React 18 的 Strict Mode 在开发环境下会故意把组件挂载 → 卸载 → 再挂载一次以暴露缺失 cleanup 的副作用没有防护的初始化代码因此执行两次checkAuthToken之类的操作第二次执行可能带来真实副作用token 失效。README.md 把这一行为单列为Runs Twice in Development故障模式其通用处方是Effect 能正常经历挂载-cleanup-再挂载循环就加 cleanup确属一次性初始化的逻辑用模块级守卫window ! undefined的模块级执行方式还能天然规避 SSR 环境。替代方案速查需求到模式的映射把九个反模式收敛后alternatives.md 给出的需求 → 正确工具映射表如下需求解决方案值来自 props/state渲染期间计算昂贵计算useMemoprop 变化时重置全部状态keyprop列表更新后保持选中存 ID 而非对象由 ID 派生对象响应用户动作事件处理器与外部系统同步useEffect cleanup订阅外部 storeuseSyncExternalStore组件间共享状态状态上提Lifting State Up数据请求带 cleanup 的自定义 Hook / 框架方案其中两个值得单独展开存 ID 而非对象把选中项对象存在 state 里列表更新后往往要再加一个 Effect 调整选中值改为存selectedId选中对象用items.find(item item.id selectedId) ?? null渲染期间派生列表刷新后只要同 ID 项目还在选中自然保持Effect 整个省掉。useSyncExternalStore订阅外部 store浏览器 API、第三方全局 store时手写的Effect 里 addEventListener cleanup 里 removeEventListener可以替换为目的性更强的 Hook。以在线状态为例import { useSyncExternalStore } from react; function subscribe(callback) { window.addEventListener(online, callback); window.addEventListener(offline, callback); return () { window.removeEventListener(online, callback); window.removeEventListener(offline, callback); }; } function useOnlineStatus() { return useSyncExternalStore( subscribe, () navigator.onLine, // 客户端值 () true // 服务端值SSR ); }仓库源码印证Plate 中的 useSyncExternalStore 实践Effect 应服务于外部系统这一宗旨在 Plate 的代码库中有真实落点。以 Plate 核心的元素状态选择器 useElementSelector.ts 为例它没有用useEffect去监听 store 变化再 setState而是把 Element Store 当作外部系统直接桥接到 Reactconst subscribe React.useCallback( (onStoreChange: () void) context?.runtime.subscribe(onStoreChange) ?? (() {}), [context] ); // ... getSnapshot 内含 selector 结果缓存与 equalityFn 比较 ... return React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);可以看到几个与本文主题直接对应的工程细节订阅外部系统而非同步 React stateruntime.subscribe(onStoreChange)通知 React 重新取快照React 自动完成变更 → 重渲染的桥接全程没有一个store 变了 → 再setState的 Effect 中转对应反模式 6、7 的数据流应单向原则快照内做记忆化与相等判断getSnapshot见 useElementSelector.ts维护cacheRef通过equalityFn比较新旧值相等则复用缓存值避免不必要的重渲染——这正是昂贵计算用缓存代替 Effect反模式 2在外部 store 场景的等价实现selector 稳定性由调用方 deps 控制React.useMemo(() [selector, equalityFn], deps)把 selector 的 memoization 交给调用方声明的依赖数组selector 变化时清空缓存cache.entry null从源码结构看这是为了在保证稳定引用的同时允许调用方精确控制失效时机。这说明能不用 Effect 就不用不是文档层面的教条而是可以直接落地为useSyncExternalStore 快照缓存的具体代码形态。实践自查清单结合 SKILL.md 与 README.md 的最佳实践在写下useEffect之前按顺序过一遍问有没有外部系统没有外部系统浏览器 API、第三方组件、网络、全局 store大概率不需要 Effect是响应用户交互吗用事件处理器——那里你确切知道发生了什么是能从 props/state 算出来的值吗渲染期间直接算昂贵则useMemo是 prop 变化要重置状态吗给组件换key确实要 Effect 时它订阅了什么、请求了什么、设了什么定时器那就必须返回 cleanup用ignore标志或AbortController处理竞态开发模式下它跑两次会出问题吗一次性初始化逻辑加模块级didInit守卫或提升到模块级执行有没有更专门的工具外部 store →useSyncExternalStore数据获取 → 框架/请求库共享状态 → 状态上提。一句话总结Effect 是 React 的逃生舱口而不是常规通道。先证明确实需要与外部系统同步再写 Effect凡是能在渲染期间算出来、能在事件处理器里处理完、能用key重置掉的逻辑都不应该进入 Effect。延伸阅读路径技能入口与决策树SKILL.md九个反模式完整对照anti-patterns.md八种替代方案与代码模板alternatives.md技能使用说明与故障模式对照README.md仓库内useSyncExternalStore实践useElementSelector.ts【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表