Join the discussion
Comments
Loading comments...
Cookie preferences
We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.
Join the discussion
Loading comments...
We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.
副作用的执行时机、清理时序和依赖项追踪。
useEffect 是将 React 组件与外部系统同步的主要 hook — 浏览器 API、第三方库、网络请求和订阅。它在浏览器绘制屏幕后运行,使其成为非阻塞的。每个 effect 可选地返回一个清理函数,React 在再次运行 effect 之前以及组件卸载时调用它。
React 提交 DOM 变更,浏览器绘制屏幕,然后 React 异步运行你的 useEffect 回调。这个顺序意味着 effects 永远不会阻塞初始渲染 — 用户立即看到内容。
React 使用 Object.is() 相等性将当前 deps 数组与上一次渲染的 deps 进行比较。如果任何 dep 发生变化,该 effect 将被调度重新运行。没有 deps 数组 = 每次渲染后运行。空数组 [] = 挂载后运行一次。
如果你的 effect 返回一个函数,React 将其存储为清理函数。在再次运行 effect 之前(因为 dep 变化),React 首先调用上一次的清理。当组件卸载时,React 调用最后一次清理。
在 StrictMode 中,React 故意挂载 → 卸载 → 重新挂载每个组件,以暴露没有正确清理的 effects。双重调用只在开发环境中发生。这就是为什么适当的清理是不可妥协的。
useLayoutEffect 与 useEffect 的语义相同,但在 DOM 变更后、浏览器绘制前同步触发。当你的 effect 需要读取 DOM 测量并在用户看到任何内容之前应用修正时使用它。
控制重新运行频率。[] = 仅挂载时。[a, b] = 当 a 或 b 变化时。没有数组 = 每次渲染。React 使用 Object.is() 比较 — 避免将对象/数组作为 deps。
从 effect 返回的函数。在 effect 重新运行(dep 变化)前和组件卸载时调用。用于取消获取、清除定时器、取消订阅。
当 effect 捕获了旧渲染中的 state/props 但永远不重新运行以获取更新值时。通过将变量添加到 deps 或使用 ref 来修复。
在 DOM 变更后、绘制前同步触发。阻塞绘制 — 保持快速。用于 DOM 测量(getBoundingClientRect、滚动位置)。
在任何 DOM 变更之前触发。供 CSS-in-JS 库在布局前同步注入样式规则。不适用于一般应用代码。
1// 1. Event subscription — must clean up2useEffect(() => {3 window.addEventListener('resize', handleResize);4 return () => window.removeEventListener('resize', handleResize);5}, []); // mount once, clean up on unmount67// 2. Fetch with cancellation (AbortController)8useEffect(() => {9 const controller = new AbortController();1011 fetch('/api/data', { signal: controller.signal })12 .then(res => res.json())13 .then(setData)14 .catch(err => {15 if (err.name !== 'AbortError') throw err; // ignore cancellation16 });1718 return () => controller.abort(); // cancel if dep changes or unmount19}, [userId]);2021// 3. useLayoutEffect for DOM measurement22useLayoutEffect(() => {23 const height = ref.current.getBoundingClientRect().height;24 setContainerHeight(height); // applied before paint — no flicker25}, [content]);
useEffect 是 React 中最常被误用的 hook。正确使用 effects — 正确的 deps、适当的清理、正确的时机 — 是构建不泄漏内存、不发出额外网络请求、不存在陈旧数据 bug 的应用的关键。心智模型的转变是:"effects 是同步机制",而不是"生命周期事件"。你告诉 React 如何与外部世界保持同步,而不是挂入挂载/更新/卸载回调。
A developer writes `useEffect(() => { fetchData() })` without a dependency array. The app makes thousands of API calls per second, the browser freezes, and the API rate limits the user.
No dependency array means the effect runs AFTER EVERY RENDER. fetchData sets state → state change triggers re-render → re-render fires the effect → effect calls fetchData → sets state → infinite loop. Each loop iteration also creates a new network request.
Add the correct dependency array: `useEffect(() => { fetchData() }, [])` for mount-only, or `useEffect(() => { fetchData(query) }, [query])` for re-fetch when query changes. The dependency array tells React's effect scheduler when to skip re-execution.
Takeaway: The dependency array is not optional decoration — it's the core control mechanism of useEffect. No array = every render. Empty array = mount only. Specific deps = run when those values change. Getting this wrong is the #1 source of useEffect bugs.
A countdown timer displays the time remaining. You use `setInterval` inside useEffect with an empty dependency array. The timer shows the same number forever instead of counting down.
The useEffect callback closes over the initial `count` value (e.g., 60). setInterval calls `setCount(count - 1)` every second, but `count` is always 60 inside the closure (captured at mount time). Result: `setCount(59)` every second — never changes from 59.
Use the functional updater form: `setCount(prev => prev - 1)`. The updater receives the CURRENT state, not the stale closure value. Alternatively, store count in a ref and sync it every render. The functional form is React's escape hatch for stale closures in effects.
Takeaway: Effects capture values at the time they're created (closures). For intervals and timeouts that need current state, always use the functional updater `setState(prev => ...)` or a ref pattern to bypass the stale closure.