When you write an Effect, the linter will verify that you’ve included every reactive value (like props and state) that the Effect reads in the list of your Effect’s dependencies. This ensures that your Effect remains synchronized with the latest props and state of your component. Unnecessary dependencies may cause your Effect to run too often, or even create an infinite loop. Follow this guide to review and remove unnecessary dependencies from your Effects.
You will learn
- How to fix infinite Effect dependency loops
- What to do when you want to remove a dependency
- How to read a value from your Effect without “reacting” to it
- How and why to avoid object and function dependencies
- Why suppressing the dependency linter is dangerous, and what to do instead
Dependencies should match the code
When you write an Effect, you first specify how tostart and stop whatever you want your Effect to be doing:
constserverUrl ='https://localhost:1234';
functionChatRoom({roomId}){
useEffect(()=>{
constconnection =createConnection(serverUrl,roomId);
connection.connect();
return()=>connection.disconnect();
// ...
}Then, if you leave the Effect dependencies empty ([]), the linter will suggest the correct dependencies:
import{useState,useEffect}from'react';import{createConnection}from'./chat.js';constserverUrl ='https://localhost:1234';functionChatRoom({roomId}){useEffect(()=>{constconnection =createConnection(serverUrl,roomId);connection.connect();return()=>connection.disconnect();},[]);// <-- Fix the mistake here!return<h1>Welcome to the{roomId} room!</h1>;}exportdefaultfunctionApp(){const[roomId,setRoomId] =useState('general');return(<><label> Choose the chat room:{' '}<selectvalue={roomId}onChange={e=>setRoomId(e.target.value)}><optionvalue="general">general</option><optionvalue="travel">travel</option><optionvalue="music">music</option></select></label><hr/><ChatRoomroomId={roomId}/></>);}
Fill them in according to what the linter says:
functionChatRoom({roomId}){
useEffect(()=>{
constconnection =createConnection(serverUrl,roomId);
connection.connect();
return()=>connection.disconnect();
},[roomId]);// ✅ All dependencies declared
// ...
}Effects “react” to reactive values. SinceroomId is a reactive value (it can change due to a re-render), the linter verifies that you’ve specified it as a dependency. IfroomId receives a different value, React will re-synchronize your Effect. This ensures that the chat stays connected to the selected room and “reacts” to the dropdown:
import{useState,useEffect}from'react';import{createConnection}from'./chat.js';constserverUrl ='https://localhost:1234';functionChatRoom({roomId}){useEffect(()=>{constconnection =createConnection(serverUrl,roomId);connection.connect();return()=>connection.disconnect();},[roomId]);return<h1>Welcome to the{roomId} room!</h1>;}exportdefaultfunctionApp(){const[roomId,setRoomId] =useState('general');return(<><label> Choose the chat room:{' '}<selectvalue={roomId}onChange={e=>setRoomId(e.target.value)}><optionvalue="general">general</option><optionvalue="travel">travel</option><optionvalue="music">music</option></select></label><hr/><ChatRoomroomId={roomId}/></>);}
To remove a dependency, prove that it’s not a dependency
Notice that you can’t “choose” the dependencies of your Effect. Everyreactive value used by your Effect’s code must be declared in your dependency list. The dependency list is determined by the surrounding code:
constserverUrl ='https://localhost:1234';
functionChatRoom({roomId}){// This is a reactive value
useEffect(()=>{
constconnection =createConnection(serverUrl,roomId);// This Effect reads that reactive value
connection.connect();
return()=>connection.disconnect();
},[roomId]);// ✅ So you must specify that reactive value as a dependency of your Effect
// ...
}Reactive values include props and all variables and functions declared directly inside of your component. SinceroomId is a reactive value, you can’t remove it from the dependency list. The linter wouldn’t allow it:
constserverUrl ='https://localhost:1234';
functionChatRoom({roomId}){
useEffect(()=>{
constconnection =createConnection(serverUrl,roomId);
connection.connect();
return()=>connection.disconnect();
},[]);// 🔴 React Hook useEffect has a missing dependency: 'roomId'
// ...
}And the linter would be right! SinceroomId may change over time, this would introduce a bug in your code.
To remove a dependency, “prove” to the linter that itdoesn’t need to be a dependency. For example, you can moveroomId out of your component to prove that it’s not reactive and won’t change on re-renders:
constserverUrl ='https://localhost:1234';
constroomId ='music';// Not a reactive value anymore
functionChatRoom(){
useEffect(()=>{
constconnection =createConnection(serverUrl,roomId);
connection.connect();
return()=>connection.disconnect();
},[]);// ✅ All dependencies declared
// ...
}Now thatroomId is not a reactive value (and can’t change on a re-render), it doesn’t need to be a dependency:
import{useState,useEffect}from'react';import{createConnection}from'./chat.js';constserverUrl ='https://localhost:1234';constroomId ='music';exportdefaultfunctionChatRoom(){useEffect(()=>{constconnection =createConnection(serverUrl,roomId);connection.connect();return()=>connection.disconnect();},[]);return<h1>Welcome to the{roomId} room!</h1>;}
This is why you could now specify anempty ([]) dependency list. Your Effectreally doesn’t depend on any reactive value anymore, so itreally doesn’t need to re-run when any of the component’s props or state change.
To change the dependencies, change the code
You might have noticed a pattern in your workflow:
- First, youchange the code of your Effect or how your reactive values are declared.
- Then, you follow the linter and adjust the dependencies tomatch the code you have changed.
- If you’re not happy with the list of dependencies, yougo back to the first step (and change the code again).
The last part is important.If you want to change the dependencies, change the surrounding code first. You can think of the dependency list asa list of all the reactive values used by your Effect’s code. You don’tchoose what to put on that list. The listdescribes your code. To change the dependency list, change the code.
This might feel like solving an equation. You might start with a goal (for example, to remove a dependency), and you need to “find” the code matching that goal. Not everyone finds solving equations fun, and the same thing could be said about writing Effects! Luckily, there is a list of common recipes that you can try below.
Pitfall
If you have an existing codebase, you might have some Effects that suppress the linter like this:
useEffect(()=>{
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
},[]);When dependencies don’t match the code, there is a very high risk of introducing bugs. By suppressing the linter, you “lie” to React about the values your Effect depends on.
Instead, use the techniques below.
Deep Dive
Suppressing the linter leads to very unintuitive bugs that are hard to find and fix. Here’s one example:
import{useState,useEffect}from'react';exportdefaultfunctionTimer(){const[count,setCount] =useState(0);const[increment,setIncrement] =useState(1);functiononTick(){setCount(count +increment);}useEffect(()=>{constid =setInterval(onTick,1000);return()=>clearInterval(id);// eslint-disable-next-line react-hooks/exhaustive-deps},[]);return(<><h1> Counter:{count}<buttononClick={()=>setCount(0)}>Reset</button></h1><hr/><p> Every second, increment by:<buttondisabled={increment ===0}onClick={()=>{setIncrement(i=>i -1);}}>–</button><b>{increment}</b><buttononClick={()=>{setIncrement(i=>i +1);}}>+</button></p></>);}
Let’s say that you wanted to run the Effect “only on mount”. You’ve read thatempty ([]) dependencies do that, so you’ve decided to ignore the linter, and forcefully specified[] as the dependencies.
This counter was supposed to increment every second by the amount configurable with the two buttons. However, since you “lied” to React that this Effect doesn’t depend on anything, React forever keeps using theonTick function from the initial render.During that render,count was0 andincrement was1. This is whyonTick from that render always callssetCount(0 + 1) every second, and you always see1. Bugs like this are harder to fix when they’re spread across multiple components.
There’s always a better solution than ignoring the linter! To fix this code, you need to addonTick to the dependency list. (To ensure the interval is only setup once,makeonTick an Effect Event.)
We recommend treating the dependency lint error as a compilation error. If you don’t suppress it, you will never see bugs like this. The rest of this page documents the alternatives for this and other cases.
Removing unnecessary dependencies
Every time you adjust the Effect’s dependencies to reflect the code, look at the dependency list. Does it make sense for the Effect to re-run when any of these dependencies change? Sometimes, the answer is “no”:
- You might want to re-executedifferent parts of your Effect under different conditions.
- You might want to only read thelatest value of some dependency instead of “reacting” to its changes.
- A dependency may change too oftenunintentionally because it’s an object or a function.
To find the right solution, you’ll need to answer a few questions about your Effect. Let’s walk through them.
Should this code move to an event handler?
The first thing you should think about is whether this code should be an Effect at all.
Imagine a form. On submit, you set thesubmitted state variable totrue. You need to send a POST request and show a notification. You’ve put this logic inside an Effect that “reacts” tosubmitted beingtrue:
functionForm(){
const[submitted,setSubmitted] =useState(false);
useEffect(()=>{
if(submitted){
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!');
}
},[submitted]);
functionhandleSubmit(){
setSubmitted(true);
}
// ...
}Later, you want to style the notification message according to the current theme, so you read the current theme. Sincetheme is declared in the component body, it is a reactive value, so you add it as a dependency:
functionForm(){
const[submitted,setSubmitted] =useState(false);
consttheme =useContext(ThemeContext);
useEffect(()=>{
if(submitted){
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!',theme);
}
},[submitted,theme]);// ✅ All dependencies declared
functionhandleSubmit(){
setSubmitted(true);
}
// ...
}By doing this, you’ve introduced a bug. Imagine you submit the form first and then switch between Dark and Light themes. Thetheme will change, the Effect will re-run, and so it will display the same notification again!
The problem here is that this shouldn’t be an Effect in the first place. You want to send this POST request and show the notification in response tosubmitting the form, which is a particular interaction. To run some code in response to particular interaction, put that logic directly into the corresponding event handler:
functionForm(){
consttheme =useContext(ThemeContext);
functionhandleSubmit(){
// ✅ Good: Event-specific logic is called from event handlers
post('/api/register');
showNotification('Successfully registered!',theme);
}
// ...
}Now that the code is in an event handler, it’s not reactive—so it will only run when the user submits the form. Read more aboutchoosing between event handlers and Effects andhow to delete unnecessary Effects.
Is your Effect doing several unrelated things?
The next question you should ask yourself is whether your Effect is doing several unrelated things.
Imagine you’re creating a shipping form where the user needs to choose their city and area. You fetch the list ofcities from the server according to the selectedcountry to show them in a dropdown:
functionShippingForm({country}){
const[cities,setCities] =useState(null);
const[city,setCity] =useState(null);
useEffect(()=>{
letignore =false;
fetch(`/api/cities?country=${country}`)
.then(response=>response.json())
.then(json=>{
if(!ignore){
setCities(json);
}
});
return()=>{
ignore =true;
};
},[country]);// ✅ All dependencies declared
// ...This is a good example offetching data in an Effect. You are synchronizing thecities state with the network according to thecountry prop. You can’t do this in an event handler because you need to fetch as soon asShippingForm is displayed and whenever thecountry changes (no matter which interaction causes it).
Now let’s say you’re adding a second select box for city areas, which should fetch theareas for the currently selectedcity. You might start by adding a secondfetch call for the list of areas inside the same Effect:
functionShippingForm({country}){
const[cities,setCities] =useState(null);
const[city,setCity] =useState(null);
const[areas,setAreas] =useState(null);
useEffect(()=>{
letignore =false;
fetch(`/api/cities?country=${country}`)
.then(response=>response.json())
.then(json=>{
if(!ignore){
setCities(json);
}
});
// 🔴 Avoid: A single Effect synchronizes two independent processes
if(city){
fetch(`/api/areas?city=${city}`)
.then(response=>response.json())
.then(json=>{
if(!ignore){
setAreas(json);
}
});
}
return()=>{
ignore =true;
};
},[country,city]);// ✅ All dependencies declared
// ...However, since the Effect now uses thecity state variable, you’ve had to addcity to the list of dependencies. That, in turn, introduced a problem: when the user selects a different city, the Effect will re-run and callfetchCities(country). As a result, you will be unnecessarily refetching the list of cities many times.
The problem with this code is that you’re synchronizing two different unrelated things:
- You want to synchronize the
citiesstate to the network based on thecountryprop. - You want to synchronize the
areasstate to the network based on thecitystate.
Split the logic into two Effects, each of which reacts to the prop that it needs to synchronize with:
functionShippingForm({country}){
const[cities,setCities] =useState(null);
useEffect(()=>{
letignore =false;
fetch(`/api/cities?country=${country}`)
.then(response=>response.json())
.then(json=>{
if(!ignore){
setCities(json);
}
});
return()=>{
ignore =true;
};
},[country]);// ✅ All dependencies declared
const[city,setCity] =useState(null);
const[areas,setAreas] =useState(null);
useEffect(()=>{
if(city){
letignore =false;
fetch(`/api/areas?city=${city}`)
.then(response=>response.json())
.then(json=>{
if(!ignore){
setAreas(json);
}
});
return()=>{
ignore =true;
};
}
},[city]);// ✅ All dependencies declared
// ...Now the first Effect only re-runs if thecountry changes, while the second Effect re-runs when thecity changes. You’ve separated them by purpose: two different things are synchronized by two separate Effects. Two separate Effects have two separate dependency lists, so they won’t trigger each other unintentionally.
The final code is longer than the original, but splitting these Effects is still correct.Each Effect should represent an independent synchronization process. In this example, deleting one Effect doesn’t break the other Effect’s logic. This means theysynchronize different things, and it’s good to split them up. If you’re concerned about duplication, you can improve this code byextracting repetitive logic into a custom Hook.
Are you reading some state to calculate the next state?
This Effect updates themessages state variable with a newly created array every time a new message arrives:
functionChatRoom({roomId}){
const[messages,setMessages] =useState([]);
useEffect(()=>{
constconnection =createConnection();
connection.connect();
connection.on('message',(receivedMessage)=>{
setMessages([...messages,receivedMessage]);
});
// ...It uses themessages variable tocreate a new array starting with all the existing messages and adds the new message at the end. However, sincemessages is a reactive value read by an Effect, it must be a dependency:
functionChatRoom({roomId}){
const[messages,setMessages] =useState([]);
useEffect(()=>{
constconnection =createConnection();
connection.connect();
connection.on('message',(receivedMessage)=>{
setMessages([...messages,receivedMessage]);
});
return()=>connection.disconnect();
},[roomId,messages]);// ✅ All dependencies declared
// ...And makingmessages a dependency introduces a problem.
Every time you receive a message,setMessages() causes the component to re-render with a newmessages array that includes the received message. However, since this Effect now depends onmessages, this willalso re-synchronize the Effect. So every new message will make the chat re-connect. The user would not like that!
To fix the issue, don’t readmessages inside the Effect. Instead, pass anupdater function tosetMessages:
functionChatRoom({roomId}){
const[messages,setMessages] =useState([]);
useEffect(()=>{
constconnection =createConnection();
connection.connect();
connection.on('message',(receivedMessage)=>{
setMessages(msgs=>[...msgs,receivedMessage]);
});
return()=>connection.disconnect();
},[roomId]);// ✅ All dependencies declared
// ...Notice how your Effect does not read themessages variable at all now. You only need to pass an updater function likemsgs => [...msgs, receivedMessage]. Reactputs your updater function in a queue and will provide themsgs argument to it during the next render. This is why the Effect itself doesn’t need to depend onmessages anymore. As a result of this fix, receiving a chat message will no longer make the chat re-connect.
Do you want to read a value without “reacting” to its changes?
Suppose that you want to play a sound when the user receives a new message unlessisMuted istrue:
functionChatRoom({roomId}){
const[messages,setMessages] =useState([]);
const[isMuted,setIsMuted] =useState(false);
useEffect(()=>{
constconnection =createConnection();
connection.connect();
connection.on('message',(receivedMessage)=>{
setMessages(msgs=>[...msgs,receivedMessage]);
if(!isMuted){
playSound();
}
});
// ...Since your Effect now usesisMuted in its code, you have to add it to the dependencies:
functionChatRoom({roomId}){
const[messages,setMessages] =useState([]);
const[isMuted,setIsMuted] =useState(false);
useEffect(()=>{
constconnection =createConnection();
connection.connect();
connection.on('message',(receivedMessage)=>{
setMessages(msgs=>[...msgs,receivedMessage]);
if(!isMuted){
playSound();
}
});
return()=>connection.disconnect();
},[roomId,isMuted]);// ✅ All dependencies declared
// ...The problem is that every timeisMuted changes (for example, when the user presses the “Muted” toggle), the Effect will re-synchronize, and reconnect to the chat. This is not the desired user experience! (In this example, even disabling the linter would not work—if you do that,isMuted would get “stuck” with its old value.)
To solve this problem, you need to extract the logic that shouldn’t be reactive out of the Effect. You don’t want this Effect to “react” to the changes inisMuted.Move this non-reactive piece of logic into an Effect Event:
import{useState,useEffect,useEffectEvent}from'react';
functionChatRoom({roomId}){
const[messages,setMessages] =useState([]);
const[isMuted,setIsMuted] =useState(false);
constonMessage =useEffectEvent(receivedMessage=>{
setMessages(msgs=>[...msgs,receivedMessage]);
if(!isMuted){
playSound();
}
});
useEffect(()=>{
constconnection =createConnection();
connection.connect();
connection.on('message',(receivedMessage)=>{
onMessage(receivedMessage);
});
return()=>connection.disconnect();
},[roomId]);// ✅ All dependencies declared
// ...Effect Events let you split an Effect into reactive parts (which should “react” to reactive values likeroomId and their changes) and non-reactive parts (which only read their latest values, likeonMessage readsisMuted).Now that you readisMuted inside an Effect Event, it doesn’t need to be a dependency of your Effect. As a result, the chat won’t re-connect when you toggle the “Muted” setting on and off, solving the original issue!
Wrapping an event handler from the props
You might run into a similar problem when your component receives an event handler as a prop:
functionChatRoom({roomId,onReceiveMessage}){
const[messages,setMessages] =useState([]);
useEffect(()=>{
constconnection =createConnection();
connection.connect();
connection.on('message',(receivedMessage)=>{
onReceiveMessage(receivedMessage);
});
return()=>connection.disconnect();
},[roomId,onReceiveMessage]);// ✅ All dependencies declared
// ...Suppose that the parent component passes adifferentonReceiveMessage function on every render:
<ChatRoom
roomId={roomId}
onReceiveMessage={receivedMessage=>{
// ...
}}
/>SinceonReceiveMessage is a dependency, it would cause the Effect to re-synchronize after every parent re-render. This would make it re-connect to the chat. To solve this, wrap the call in an Effect Event:
functionChatRoom({roomId,onReceiveMessage}){
const[messages,setMessages] =useState([]);
constonMessage =useEffectEvent(receivedMessage=>{
onReceiveMessage(receivedMessage);
});
useEffect(()=>{
constconnection =createConnection();
connection.connect();
connection.on('message',(receivedMessage)=>{
onMessage(receivedMessage);
});
return()=>connection.disconnect();
},[roomId]);// ✅ All dependencies declared
// ...Effect Events aren’t reactive, so you don’t need to specify them as dependencies. As a result, the chat will no longer re-connect even if the parent component passes a function that’s different on every re-render.
Separating reactive and non-reactive code
In this example, you want to log a visit every timeroomId changes. You want to include the currentnotificationCount with every log, but youdon’t want a change tonotificationCount to trigger a log event.
The solution is again to split out the non-reactive code into an Effect Event:
functionChat({roomId,notificationCount}){
constonVisit =useEffectEvent(visitedRoomId=>{
logVisit(visitedRoomId,notificationCount);
});
useEffect(()=>{
onVisit(roomId);
},[roomId]);// ✅ All dependencies declared
// ...
}You want your logic to be reactive with regards toroomId, so you readroomId inside of your Effect. However, you don’t want a change tonotificationCount to log an extra visit, so you readnotificationCount inside of the Effect Event.Learn more about reading the latest props and state from Effects using Effect Events.
Does some reactive value change unintentionally?
Sometimes, youdo want your Effect to “react” to a certain value, but that value changes more often than you’d like—and might not reflect any actual change from the user’s perspective. For example, let’s say that you create anoptions object in the body of your component, and then read that object from inside of your Effect:
functionChatRoom({roomId}){
// ...
constoptions ={
serverUrl:serverUrl,
roomId:roomId
};
useEffect(()=>{
constconnection =createConnection(options);
connection.connect();
// ...This object is declared in the component body, so it’s areactive value. When you read a reactive value like this inside an Effect, you declare it as a dependency. This ensures your Effect “reacts” to its changes:
// ...
useEffect(()=>{
constconnection =createConnection(options);
connection.connect();
return()=>connection.disconnect();
},[options]);// ✅ All dependencies declared
// ...It is important to declare it as a dependency! This ensures, for example, that if theroomId changes, your Effect will re-connect to the chat with the newoptions. However, there is also a problem with the code above. To see it, try typing into the input in the sandbox below, and watch what happens in the console:
import{useState,useEffect}from'react';import{createConnection}from'./chat.js';constserverUrl ='https://localhost:1234';functionChatRoom({roomId}){const[message,setMessage] =useState('');// Temporarily disable the linter to demonstrate the problem// eslint-disable-next-line react-hooks/exhaustive-depsconstoptions ={serverUrl:serverUrl,roomId:roomId};useEffect(()=>{constconnection =createConnection(options);connection.connect();return()=>connection.disconnect();},[options]);return(<><h1>Welcome to the{roomId} room!</h1><inputvalue={message}onChange={e=>setMessage(e.target.value)}/></>);}exportdefaultfunctionApp(){const[roomId,setRoomId] =useState('general');return(<><label> Choose the chat room:{' '}<selectvalue={roomId}onChange={e=>setRoomId(e.target.value)}><optionvalue="general">general</option><optionvalue="travel">travel</option><optionvalue="music">music</option></select></label><hr/><ChatRoomroomId={roomId}/></>);}
In the sandbox above, the input only updates themessage state variable. From the user’s perspective, this should not affect the chat connection. However, every time you update themessage, your component re-renders. When your component re-renders, the code inside of it runs again from scratch.
A newoptions object is created from scratch on every re-render of theChatRoom component. React sees that theoptions object is adifferent object from theoptions object created during the last render. This is why it re-synchronizes your Effect (which depends onoptions), and the chat re-connects as you type.
This problem only affects objects and functions. In JavaScript, each newly created object and function is considered distinct from all the others. It doesn’t matter that the contents inside of them may be the same!
// During the first render
constoptions1 ={serverUrl:'https://localhost:1234',roomId:'music'};
// During the next render
constoptions2 ={serverUrl:'https://localhost:1234',roomId:'music'};
// These are two different objects!
console.log(Object.is(options1,options2));// falseObject and function dependencies can make your Effect re-synchronize more often than you need.
This is why, whenever possible, you should try to avoid objects and functions as your Effect’s dependencies. Instead, try moving them outside the component, inside the Effect, or extracting primitive values out of them.
Move static objects and functions outside your component
If the object does not depend on any props and state, you can move that object outside your component:
constoptions ={
serverUrl:'https://localhost:1234',
roomId:'music'
};
functionChatRoom(){
const[message,setMessage] =useState('');
useEffect(()=>{
constconnection =createConnection(options);
connection.connect();
return()=>connection.disconnect();
},[]);// ✅ All dependencies declared
// ...This way, youprove to the linter that it’s not reactive. It can’t change as a result of a re-render, so it doesn’t need to be a dependency. Now re-renderingChatRoom won’t cause your Effect to re-synchronize.
This works for functions too:
functioncreateOptions(){
return{
serverUrl:'https://localhost:1234',
roomId:'music'
};
}
functionChatRoom(){
const[message,setMessage] =useState('');
useEffect(()=>{
constoptions =createOptions();
constconnection =createConnection(options);
connection.connect();
return()=>connection.disconnect();
},[]);// ✅ All dependencies declared
// ...SincecreateOptions is declared outside your component, it’s not a reactive value. This is why it doesn’t need to be specified in your Effect’s dependencies, and why it won’t ever cause your Effect to re-synchronize.
Move dynamic objects and functions inside your Effect
If your object depends on some reactive value that may change as a result of a re-render, like aroomId prop, you can’t pull itoutside your component. You can, however, move its creationinside of your Effect’s code:
constserverUrl ='https://localhost:1234';
functionChatRoom({roomId}){
const[message,setMessage] =useState('');
useEffect(()=>{
constoptions ={
serverUrl:serverUrl,
roomId:roomId
};
constconnection =createConnection(options);
connection.connect();
return()=>connection.disconnect();
},[roomId]);// ✅ All dependencies declared
// ...Now thatoptions is declared inside of your Effect, it is no longer a dependency of your Effect. Instead, the only reactive value used by your Effect isroomId. SinceroomId is not an object or function, you can be sure that it won’t beunintentionally different. In JavaScript, numbers and strings are compared by their content:
// During the first render
constroomId1 ='music';
// During the next render
constroomId2 ='music';
// These two strings are the same!
console.log(Object.is(roomId1,roomId2));// trueThanks to this fix, the chat no longer re-connects if you edit the input:
import{useState,useEffect}from'react';import{createConnection}from'./chat.js';constserverUrl ='https://localhost:1234';functionChatRoom({roomId}){const[message,setMessage] =useState('');useEffect(()=>{constoptions ={serverUrl:serverUrl,roomId:roomId};constconnection =createConnection(options);connection.connect();return()=>connection.disconnect();},[roomId]);return(<><h1>Welcome to the{roomId} room!</h1><inputvalue={message}onChange={e=>setMessage(e.target.value)}/></>);}exportdefaultfunctionApp(){const[roomId,setRoomId] =useState('general');return(<><label> Choose the chat room:{' '}<selectvalue={roomId}onChange={e=>setRoomId(e.target.value)}><optionvalue="general">general</option><optionvalue="travel">travel</option><optionvalue="music">music</option></select></label><hr/><ChatRoomroomId={roomId}/></>);}
However, itdoes re-connect when you change theroomId dropdown, as you would expect.
This works for functions, too:
constserverUrl ='https://localhost:1234';
functionChatRoom({roomId}){
const[message,setMessage] =useState('');
useEffect(()=>{
functioncreateOptions(){
return{
serverUrl:serverUrl,
roomId:roomId
};
}
constoptions =createOptions();
constconnection =createConnection(options);
connection.connect();
return()=>connection.disconnect();
},[roomId]);// ✅ All dependencies declared
// ...You can write your own functions to group pieces of logic inside your Effect. As long as you also declare theminside your Effect, they’re not reactive values, and so they don’t need to be dependencies of your Effect.
Read primitive values from objects
Sometimes, you may receive an object from props:
functionChatRoom({options}){
const[message,setMessage] =useState('');
useEffect(()=>{
constconnection =createConnection(options);
connection.connect();
return()=>connection.disconnect();
},[options]);// ✅ All dependencies declared
// ...The risk here is that the parent component will create the object during rendering:
<ChatRoom
roomId={roomId}
options={{
serverUrl:serverUrl,
roomId:roomId
}}
/>This would cause your Effect to re-connect every time the parent component re-renders. To fix this, read information from the objectoutside the Effect, and avoid having object and function dependencies:
functionChatRoom({options}){
const[message,setMessage] =useState('');
const{roomId,serverUrl} =options;
useEffect(()=>{
constconnection =createConnection({
roomId:roomId,
serverUrl:serverUrl
});
connection.connect();
return()=>connection.disconnect();
},[roomId,serverUrl]);// ✅ All dependencies declared
// ...The logic gets a little repetitive (you read some values from an object outside an Effect, and then create an object with the same values inside the Effect). But it makes it very explicit what information your Effectactually depends on. If an object is re-created unintentionally by the parent component, the chat would not re-connect. However, ifoptions.roomId oroptions.serverUrl really are different, the chat would re-connect.
Calculate primitive values from functions
The same approach can work for functions. For example, suppose the parent component passes a function:
<ChatRoom
roomId={roomId}
getOptions={()=>{
return{
serverUrl:serverUrl,
roomId:roomId
};
}}
/>To avoid making it a dependency (and causing it to re-connect on re-renders), call it outside the Effect. This gives you theroomId andserverUrl values that aren’t objects, and that you can read from inside your Effect:
functionChatRoom({getOptions}){
const[message,setMessage] =useState('');
const{roomId,serverUrl} =getOptions();
useEffect(()=>{
constconnection =createConnection({
roomId:roomId,
serverUrl:serverUrl
});
connection.connect();
return()=>connection.disconnect();
},[roomId,serverUrl]);// ✅ All dependencies declared
// ...This only works forpure functions because they are safe to call during rendering. If your function is an event handler, but you don’t want its changes to re-synchronize your Effect,wrap it into an Effect Event instead.
Recap
- Dependencies should always match the code.
- When you’re not happy with your dependencies, what you need to edit is the code.
- Suppressing the linter leads to very confusing bugs, and you should always avoid it.
- To remove a dependency, you need to “prove” to the linter that it’s not necessary.
- If some code should run in response to a specific interaction, move that code to an event handler.
- If different parts of your Effect should re-run for different reasons, split it into several Effects.
- If you want to update some state based on the previous state, pass an updater function.
- If you want to read the latest value without “reacting” it, extract an Effect Event from your Effect.
- In JavaScript, objects and functions are considered different if they were created at different times.
- Try to avoid object and function dependencies. Move them outside the component or inside the Effect.
Challenge 1 of 4:Fix a resetting interval
This Effect sets up an interval that ticks every second. You’ve noticed something strange happening: it seems like the interval gets destroyed and re-created every time it ticks. Fix the code so that the interval doesn’t get constantly re-created.
import{useState,useEffect}from'react';exportdefaultfunctionTimer(){const[count,setCount] =useState(0);useEffect(()=>{console.log('✅ Creating an interval');constid =setInterval(()=>{console.log('⏰ Interval tick');setCount(count +1);},1000);return()=>{console.log('❌ Clearing an interval');clearInterval(id);};},[count]);return<h1>Counter:{count}</h1>}