Sign in
Topics
Generate your React app with prompts or Figma
When should a React effect run? Small tweaks in the dependency array can mean smooth updates or endless loops. Learn how to pinpoint the right dependencies with clear examples and visuals.
React applications can behave unpredictably when effects trigger at the wrong time. A harmless change can suddenly create an endless render loop, or a missing dependency warning can break your focus.
What determines the exact moment an effect should run?
Often, the answer lies in the useEffect dependency array. An overlooked value or an extra reference can result in stale state, repeated updates, or no updates at all.
This blog provides a clear, structured explanation of the concept, supported by practical code samples, visual diagrams, and proven strategies to help your components work as intended.
The dependency array is the second parameter you pass to the useEffect hook. It tells React when to re-run your useEffect callback function. If you skip it entirely, your effect will run after every render cycle.
Here’s a concrete example:
1import React, { useEffect, useState } from 'react'; 2 3export default function Counter() { 4 const [count, setCount] = useState(0); // state variable 5 6 useEffect(() => { 7 console.log(`Count changed to: ${count}`); 8 }, [count]); // dependency array 9 10 return ( 11 <button onClick={() => setCount(count + 1)}> 12 Count is {count} 13 </button> 14 ); 15} 16
In this example:
If you leave out the dependency array, this effect would run after all component renders, even when nothing relevant has changed. That’s an unnecessary re execution that can hurt performance.
Passing an empty dependency array ([]) means the effect runs only on the initial render. This is often used when you need one-time logic, like setting up a subscription to an external system or running a fetch data call once.
Example:
1useEffect(() => { 2 console.log('Runs once on mount'); 3}, []); // empty array 4
This approach is common when working with APIs. The effect executes once, so you don’t have to worry about triggering multiple calls when state variables change.
The dependency array acts as a change detector for your effect. In each render cycle, React does three things:
This helps you visualize how React decides whether to run the useEffect callback function. Understanding this flow makes it easier to avoid unnecessary re executions.
The React hooks exhaustive deps ESLint rule warns when your effect uses variables not listed in the dependency array. If you omit dependencies, your effect might use stale values because it closes over the previous value from the previous renders.
Example of a missing dependency problem:
1useEffect(() => { 2 console.log(count); // count not listed 3}, []); // empty dependency array 4
In this case, the log shows the initial value of count forever, even after count changes. This is why ESLint flags it.
Mistake | Cause | Fix |
---|---|---|
Infinite re renders | new object or new function created on every render | Use usecallback hook or memoize |
Missing dependencies | Ignored ESLint warnings | Add all the variables used inside effect |
Stale value | Incorrect empty array usage | Include variable in dependency list |
Fetching data loop | Effect triggers fetch data on every render | Add proper state variables or conditions |
Each mistake above comes from misunderstanding how dependencies are tracked. For example, creating a new object inside the component scope causes a new reference every time, which React treats as a change.
Empty array and no array are not the same.
Example with async function and empty array:
1useEffect(() => { 2 const getData = async () => { 3 const res = await fetch('/api/data'); // fetching data 4 console.log(await res.json()); 5 }; 6 getData(); 7}, []); // empty dependency array 8
The empty dependency array ensures this runs only once, preventing multiple API calls.
When your effect interacts with an external system, sets up timers, or attaches event handlers, you should return a cleanup function. This function removes the side effect when the component unmounts or before the effect runs again.
Example:
1useEffect(() => { 2 const id = setInterval(() => console.log('tick'), 1000); 3 return () => clearInterval(id); // cleanup function 4}, []); 5
Cleanup functions prevent memory leaks and unwanted background processes.
Effects run after state variable updates that cause a re-render. The dependency array helps you control which state variables trigger these runs.
Example with multiple state variables:
1useEffect(() => { 2 console.log('User updated'); 3}, [user]); // only runs when user changes 4
Here, even if other state variables in the component change, this effect will not run unless user changes.
Functions defined inside a component and objects inside a dependency array can trigger effects on every render. That’s because they are new references each time.
Example with function:
1const handler = () => console.log('clicked'); 2useEffect(() => { 3 window.addEventListener('click', handler); 4 return () => window.removeEventListener('click', handler); 5}, [handler]); // re runs every render unless memoized 6
To prevent this, wrap the handler in a useCallback hook so it’s stable between renders.
Extracting a useEffect call into a custom hook keeps components clean and reusable.
1function useUserData(userId) { 2 const [data, setData] = useState(null); 3 useEffect(() => { 4 const fetchUser = async () => { 5 const res = await fetch(`/api/users/${userId}`); 6 setData(await res.json()); 7 }; 8 fetchUser(); 9 }, [userId]); // dependency array 10 return data; 11} 12 13function Profile({ userId }) { 14 const user = useUserData(userId); 15 return <div>{user?.name}</div>; 16} 17
The parent component doesn’t need to handle fetching data logic directly.
The ESLint rule for React Hooks exhaustive deps enforces adding every variable referenced inside the effect to the dependency list. Disabling this rule often leads to subtle bugs. The React team recommends following it unless you fully understand the render cycle and are intentionally skipping dependencies.
An infinite loop in a useEffect hook usually happens when the effect updates a state variable that is in its dependency array. Each update triggers a new render cycle, which runs the effect again, repeating forever.
To prevent this:
You can’t directly make the use effect callback function async. Instead, define an async function inside it or outside the component scope. This pattern is common when fetching data or working with external system APIs.
Example:
1useEffect(() => { 2 async function loadData() { 3 const response = await fetch('/api/data'); 4 const result = await response.json(); 5 console.log(result); 6 } 7 loadData(); 8}, []); 9
Sometimes you want to omit dependencies intentionally, for example, when you only care about the initial value of a variable. Be aware that this can create bugs if the variable is expected to change. ESLint will warn you in such cases.
Managing effects and dependency arrays is just one part of working with React. With Rocket.new , you can build any app with simple prompts — no code required. Stop spending hours managing dependency lists and start building features faster.
The useEffect dependency array gives you control over when effects run. Correct use stops endless loops and avoids wasted re-renders. It keeps your state updates current and your component behavior consistent.
By adding the right dependencies, React knows exactly when to run or skip an effect. Using cleanup functions at the right time prevents memory leaks and unwanted actions. Careful dependency management makes your components easier to read, test, and maintain over time. Following React’s exhaustive deps rule helps your code stay stable and your app run smoothly.