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.
挂载、更新和卸载阶段 — 查看每个阶段触发的 Hooks 和副作用。
每个 React 组件都经历三个生命周期阶段:挂载(第一次渲染进入 DOM)、更新(由 state 或 prop 变化引起的重新渲染)和卸载(从 DOM 中移除)。使用 hooks 时,这些阶段通过 useEffect、useLayoutEffect 和清理函数来表达,而非类生命周期方法。
React 调用函数组件,计算 JSX,并将生成的 DOM 节点插入。useState 初始化器运行,useRef 对象被创建。DOM 插入后,useLayoutEffect 同步运行,然后 useEffect 在绘制后异步运行。
当 setState 或新的 prop 到达时,React 将重新渲染加入队列。重新渲染期间,hooks 按挂载时相同的顺序被调用。React 比较新旧输出并修补 DOM。在重新运行 effects 之前,React 会运行上一次渲染的 effects 清理函数。
在每次 effect 重新运行之前(以及卸载时),React 调用上一个 effect 的清理函数。这对于清除定时器、取消获取请求和取消订阅事件至关重要。
当组件被移除(条件渲染、路由切换等)时,React 运行所有清理函数 — useEffect 清理,然后是 useLayoutEffect 清理。DOM 节点从文档中移除。
空的 deps 数组:挂载后运行一次。清理在卸载时运行。等同于 componentDidMount + componentWillUnmount。
挂载后运行,且每当 dep 变化时运行。清理在每次重新运行前和卸载时运行。
没有 deps 数组:每次渲染后运行。这很少是你想要的 — 如果它触发 state 变化会导致无限循环。
与 useEffect 签名相同,但在 DOM 变更后、绘制前同步触发。用于 DOM 测量。
从 useEffect 回调返回的函数。React 在下次 effect 运行前和卸载时调用它。
1function Component({ userId }) {2 const [data, setData] = useState(null);34 useEffect(() => {5 // Runs after mount and when userId changes6 let cancelled = false;78 fetch(`/api/user/${userId}`)9 .then(res => res.json())10 .then(json => {11 if (!cancelled) setData(json); // guard against stale closure12 });1314 // Cleanup: cancel if userId changes before fetch resolves15 return () => { cancelled = true; };16 }, [userId]); // re-runs when userId changes1718 // Runs on unmount19 useEffect(() => {20 return () => console.log('Component unmounted');21 }, []);22}
理解生命周期时序可以防止最常见的 React bug:获取数据不带清理(导致在已卸载组件上调用 setState 的警告)、忘记清除定时器(导致内存泄漏)、以及由于误用依赖数组而过多或过少运行 effects。
A user profile page fetches user data on mount. If a user rapidly clicks through profiles (User A → User B → User C), the displayed data sometimes shows User A's data on User C's page.
Each navigation triggers a re-mount with a new userId. The useEffect fires 3 fetch calls, but they resolve out of order: User B (fast) → User C (fast) → User A (slow). The last-resolving fetch (User A) overwrites the correct data.
Use the cleanup function to set a `cancelled` flag. When userId changes, the previous effect's cleanup runs before the new effect, marking the old fetch as stale. The guard `if (!cancelled) setData(json)` prevents stale responses from updating state.
Takeaway: The cleanup function in useEffect isn't just for unmounting — it runs before EVERY re-execution of the effect. This makes it the perfect place to cancel async operations and prevent race conditions.
A chart component adds a `window.resize` event listener on mount to recalculate dimensions. After navigating away, the browser's memory usage keeps climbing and console shows 'setState on unmounted component' warnings.
The event listener was added in useEffect but never cleaned up. The component unmounts, but the listener still holds a reference to the component's state setter, creating a memory leak and triggering warnings on resize.
Return a cleanup function from useEffect that removes the event listener. The cleanup runs during the Unmount phase, before DOM removal, ensuring no orphaned listeners persist. Apply the same pattern for WebSocket connections, interval timers, and IntersectionObserver instances.
Takeaway: Every subscription, listener, or timer created in useEffect MUST have a corresponding cleanup. Think of mount/unmount as a pair: every 'set up' action needs a corresponding 'tear down' action.