When multiple dependencies of a React useEffect()
hook change within the same render cycle, its callback will run only once. However, if they change across separate render cycles, then the effect will be triggered separately for each changed dependency.
For example, when a reactive value and a value dependent on it, are both specified as useEffect()
dependencies, the effect will be triggered only once (even though both values change together):
function App() { const [count, setCount] = useState(0); const text = `clicked ${count} times!`; useEffect(() => { console.log('Effect triggered'); }, [count, text]); return ( <div> <p>count: {count}</p> <p>text: {text}</p> <button onClick={() => setCount(count + 1)}>Update</button> </div> ); }
Similarly, when multiple state variables are set in the body of the same (non-async and non-timer) function, the effect is run only once:
function App() { const [count, setCount] = useState(0); const [text, setText] = useState(''); useEffect(() => { console.log('Effect triggered'); }, [count, text]); const handleButtonClick = () => { setCount(count + 1); setText(Math.random()); }; return ( <div> <p>count: {count}</p> <p>text: {text}</p> <button onClick={handleButtonClick}>Update</button> </div> ); }
That being said, the effect may run multiple times in some cases such as when state updates are triggered from asynchronous operations (like API calls), or timer functions (like setTimeout()
, etc.).
For example, in the following code the useEffect()
callback is triggered twice everytime the button is clicked on:
function App() { const [count, setCount] = useState(0); const [text, setText] = useState(''); useEffect(() => { console.log('Effect triggered'); }, [count, text]); const handleButtonClick = () => { setTimeout(() => { setCount(count + 1); setText(Math.random()); }, 0); }; return ( <div> <p>count: {count}</p> <p>text: {text}</p> <button onClick={handleButtonClick}>Update</button> </div> ); }
Similarly, if you set multiple state variables from inside an async
function, it will run multiple times:
function App() { const [id, setId] = useState(0); const [title, setTitle] = useState(''); useEffect(() => { console.log('Effect triggered'); }, [id, title]); const handleButtonClick = async () => { const response = await fetch('https://jsonplaceholder.typicode.com/todos/1'); const data = await response.json(); setId(data.id + Math.random()); setTitle(data.title + Math.random()); }; return ( <div> <p>id: {id}</p> <p>title: {title}</p> <button onClick={handleButtonClick}>Update</button> </div> ); }
As you can see in these examples, if there are multiple state setter functions called within the same async
or timer function body, React will process them individually. This means that each state update will trigger a re-render of the affected components, causing the associated useEffect()
callback to be triggered multiple times. This is to ensure that the UI remains responsive and is updated in a timely manner.
If React were to batch all state updates (specified in async
or timer functions) within a single rendering cycle, it could cause delays in rendering and prevent the UI from being instantly updated as results of such functions may not be immediately available. This would result in a less responsive user experience, especially when there are time-consuming async
or timed operations, or a large number of state updates. To avoid this, React processes each state update (in an asynchronous operation or timer function) individually. This allows the UI to provide immediate feedback to the user by triggering re-renders of the relevant components as soon as possible.
Please note that in some instances, if the effect is running multiple times for the same set of dependencies, it could indicate a potential issue in your code, such as unintended side effects or incorrect dependencies specified in the dependency array.
This post was published by Daniyal Hamid. Daniyal currently works as the Head of Engineering in Germany and has 20+ years of experience in software engineering, design and marketing. Please show your love and support by sharing this post.