React wants your components to be predictable. Give it the same state and props, and it should paint the same UI every time. But most real applications cannot survive inside that bubble. They need to reach out. A dashboard needs fresh numbers from a server. A chat widget needs to listen for messages. A timer needs to tick. These operations are side effects, and they sit outside React's render cycle. The useEffect hook is where you put that messy, unpredictable work so your component itself stays honest.
Side Effects: What Belongs Inside useEffect
A side effect is anything that touches the world beyond returning JSX. React's render phase should be pure. When you start fetching data, writing to global variables, or attaching listeners to the DOM, you have stepped out of pure territory.
Common examples include:
- Fetching data from an API
- Setting up timers or intervals
- Adding event listeners to
windowordocument - Updating the browser tab title
- Connecting to WebSockets
These tasks share one trait: they do not belong inside your component's return statement or during the main render logic. Attempting to call a browser API like setInterval directly inside the render body will fire on every render, creating duplicate timers and confusing behavior. useEffect exists precisely to isolate this work and run it at the correct moment.
How the Dependency Array Controls Timing
The second argument to useEffect is the dependency array, and it is the single biggest source of confusion for developers moving from class components. Think of it as a set of variables React watches to decide whether to skip or execute your effect after the current render.
There are three patterns you will use repeatedly.
No dependency array at all. If you omit the array entirely, React assumes you want the effect to run after every single render, including the first one. This is rarely what you need. If your effect performs a network request or a heavy DOM operation, running it on every keystroke or state tweak will tank performance. Use this pattern only when you truly need to re-run something because any prop or state might have changed and you cannot specify which ones.
An empty array []. This tells React to run the effect once, immediately after the component mounts and the DOM is ready. This is the right place for initial data fetches. For example, if your component loads user profile data, you want that request to fire exactly once when the profile page appears, not every time the user interacts with a form lower down the page.
An array with specific variables [count]. This is the precision tool. React compares the current values of these dependencies against their values during the last render. If any of them changed, the effect runs. If nothing in the list changed, React skips the effect entirely.
If you are syncing the browser tab title with a state variable, you would put that variable in the dependency array. React will then update the title only when that value shifts. Leave it out, and the title stays stale. Put unrelated state variables in, and you waste cycles updating the title for changes that do not matter.
Cleanup Is Not Optional
Some effects leave footprints behind. A timer keeps counting. An event listener keeps firing. A WebSocket stays open. When your component unmounts, or even when an effect re-runs because its dependencies changed, React does not automatically clean up the previous effect's residue. That is your job.
You create a cleanup function by returning a function from inside useEffect. React calls this cleanup before it applies the next effect, and once more when the component leaves the screen.
You should use cleanup for:
- Clearing intervals or timeouts with
clearIntervalorclearTimeout - Removing event listeners added to
window,document, or external nodes - Unsubscribing from data streams or services
Neglect this, and you get memory leaks. A component mounts, attaches a scroll listener, unmounts, and the listener remains. The browser holds onto the callback and the DOM nodes it references. Over time, especially in single-page applications with heavy navigation, these ghosts accumulate and slow down the tab. The fix is usually just a few lines: return a function that removes what you added.
Common Mistakes That Ship to Production
即使是经验丰富的开发者,在有更简单方案的情况下,也往往会习惯性地使用 useEffect。以下三种模式在代码审查(code review)时应当引起警惕。
无限循环。 除非你设置了能够打破循环的门控条件(gate condition),否则永远不要在 useEffect 内部更新一个同时出现在依赖数组(dependency array)中的 state 变量。如果你读取 count,对其进行递增,并将 count 列为依赖项,React 会检测到变化、重新渲染、再次运行 effect、再次递增,最终导致浏览器卡死。
不必要的 effect。 不要使用 useEffect 根据现有的 props 或 state 来计算一个值。如果你可以在渲染期间直接推导出该值,那就直接做。派生值(Derived values)应该放在组件主体中,或者通过 useMemo 进行记忆化计算。将它们移入 effect 会将逻辑拆分到渲染阶段和 effect 阶段,不仅没有任何好处,还会使代码变得难以理解。
用于用户操作的错误工具。 `use
