Lifecycle of Reactive Effects
Effects have a different lifecycle from components. Components may mount, update, or unmount. An Effect can only do two things: to start synchronizing something, and later to stop synchronizing it. This cycle can happen multiple times if your Effect depends on props and state that change over time. React provides a linter rule to check that youâve specified your Effectâs dependencies correctly. This keeps your Effect synchronized to the latest props and state.
You will learn
- How an Effectâs lifecycle is different from a componentâs lifecycle
- How to think about each individual Effect in isolation
- When your Effect needs to re-synchronize, and why
- How your Effectâs dependencies are determined
- What it means for a value to be reactive
- What an empty dependency array means
- How React verifies your dependencies are correct with a linter
- What to do when you disagree with the linter
The lifecycle of an Effect
Every React component goes through the same lifecycle:
- A component mounts when itâs added to the screen.
- A component updates when it receives new props or state. This usually happens in response to an interaction.
- A component unmounts when itâs removed from the screen.
Itâs a good way to think about components, but not about Effects. Instead, try to think about each Effect independently from your componentâs lifecycle. An Effect describes how to synchronize an external system to the current props and state. As your code changes, this synchronization will need to happen more or less often.
To illustrate this point, consider this Effect connecting your component to a chat server:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
Your Effectâs body specifies how to start synchronizing:
// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
// ...
The cleanup function returned by your Effect specifies how to stop synchronizing:
// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
// ...
Intuitively, you might think that React would start synchronizing when your component mounts and stop synchronizing when your component unmounts. However, this is not the end of the story! Sometimes, it may also be necessary to start and stop synchronizing multiple times while the component remains mounted.
Letâs look at why this is necessary, when it happens, and how you can control this behavior.
Why synchronization may need to happen more than once
Imagine this ChatRoom
component receives a roomId
prop that the user picks in a dropdown. Letâs say that initially the user picks the "general"
room as the roomId
. Your app displays the "general"
chat room:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId /* "general" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}
After the UI is displayed, React will run your Effect to start synchronizing. It connects to the "general"
room:
function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
}, [roomId]);
// ...
So far, so good.
Later, the user picks a different room in the dropdown (for example, "travel"
). First, React will update the UI:
function ChatRoom({ roomId /* "travel" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}
Pause to think about what should happen next. The user sees that "travel"
is the selected chat room in the UI. However, the Effect that ran the last time is still connected to the "general"
room. The roomId
prop has changed, so whatever your Effect did back then (connecting to the "general"
room) no longer matches the UI.
At this point, you want React to do two things:
- Stop synchronizing with the old
roomId
(disconnect from the"general"
room) - Start synchronizing with the new
roomId
(connect to the"travel"
room)
Luckily, youâve already taught React how to do both of these things! Your Effectâs body specifies how to start synchronizing, and your cleanup function specifies how to stop synchronizing. All that React needs to do now is to call them in the correct order and with the correct props and state. Letâs see how exactly that happens.
How React re-synchronizes your Effect
Recall that your ChatRoom
component has received a new value for its roomId
prop. It used to be "general"
, and now it is "travel"
. React needs to re-synchronize your Effect to re-connect you to a different room.
To stop synchronizing, React will call the cleanup function that your Effect returned after connecting to the "general"
room. Since roomId
was "general"
, the cleanup function disconnects from the "general"
room:
function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
// ...
Then React will run the Effect that youâve provided during this render. This time, roomId
is "travel"
so it will start synchronizing to the "travel"
chat room (until its cleanup function is eventually called too):
function ChatRoom({ roomId /* "travel" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "travel" room
connection.connect();
// ...
Thanks to this, youâre now connected to the same room that the user chose in the UI. Disaster averted!
Every time after your component re-renders with a different roomId
, your Effect will re-synchronize. For example, letâs say the user changes roomId
from "travel"
to "music"
. React will again stop synchronizing your Effect by calling its cleanup function (disconnecting you from the "travel"
room). Then it will start synchronizing again by running its body with the new roomId
prop (connecting you to the "music"
room).
Finally, when the user goes to a different screen, ChatRoom
unmounts. Now there is no need to stay connected at all. React will stop synchronizing your Effect one last time and disconnect you from the "music"
chat room.
Thinking from the Effectâs perspective
Letâs recap everything thatâs happened from the ChatRoom
componentâs perspective:
ChatRoom
mounted withroomId
set to"general"
ChatRoom
updated withroomId
set to"travel"
ChatRoom
updated withroomId
set to"music"
ChatRoom
unmounted
During each of these points in the componentâs lifecycle, your Effect did different things:
- Your Effect connected to the
"general"
room - Your Effect disconnected from the
"general"
room and connected to the"travel"
room - Your Effect disconnected from the
"travel"
room and connected to the"music"
room - Your Effect disconnected from the
"music"
room
Now letâs think about what happened from the perspective of the Effect itself:
useEffect(() => {
// Your Effect connected to the room specified with roomId...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
// ...until it disconnected
connection.disconnect();
};
}, [roomId]);
This codeâs structure might inspire you to see what happened as a sequence of non-overlapping time periods:
- Your Effect connected to the
"general"
room (until it disconnected) - Your Effect connected to the
"travel"
room (until it disconnected) - Your Effect connected to the
"music"
room (until it disconnected)
Previously, you were thinking from the componentâs perspective. When you looked from the componentâs perspective, it was tempting to think of Effects as âcallbacksâ or âlifecycle eventsâ that fire at a specific time like âafter a renderâ or âbefore unmountâ. This way of thinking gets complicated very fast, so itâs best to avoid it.
Instead, always focus on a single start/stop cycle at a time. It shouldnât matter whether a component is mounting, updating, or unmounting. All you need to do is to describe how to start synchronization and how to stop it. If you do it well, your Effect will be resilient to being started and stopped as many times as itâs needed.
This might remind you how you donât think whether a component is mounting or updating when you write the rendering logic that creates JSX. You describe what should be on the screen, and React figures out the rest.
How React verifies that your Effect can re-synchronize
Here is a live example that you can play with. Press âOpen chatâ to mount the ChatRoom
component:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Welcome to the {roomId} room!</h1>; } export default function App() { const [roomId, setRoomId] = useState('general'); const [show, setShow] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <button onClick={() => setShow(!show)}> {show ? 'Close chat' : 'Open chat'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Notice that when the component mounts for the first time, you see three logs:
â Connecting to "general" room at https://localhost:1234...
(development-only)â Disconnected from "general" room at https://localhost:1234.
(development-only)â Connecting to "general" room at https://localhost:1234...
The first two logs are development-only. In development, React always remounts each component once. In other words, React verifies that your Effect can re-synchronize by forcing it to do that immediately in development. This might remind you how you might open the door and close it an extra time to check that the door lock works. React starts and stops your Effect one extra time in development to check youâve implemented its cleanup well.
The main reason your Effect will re-synchronize in practice is if some data it uses has changed. In the sandbox above, change the selected chat room. Notice how, when the roomId
changes, your Effect re-synchronizes.
However, there are also more unusual cases in which re-synchronization is necessary. For example, try editing the serverUrl
in the sandbox above while the chat is open. Notice how the Effect re-synchronizes in response to your edits to the code. In the future, React may add more features that take advantage of re-synchronization.
How React knows that it needs to re-synchronize the Effect
You might be wondering how React knew that your Effect needed to re-synchronize after roomId
changes. Itâs because you told React that this Effectâs code depends on roomId
by including it in the list of dependencies:
function ChatRoom({ roomId }) { // The roomId prop may change over time
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads roomId
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // So you tell React that this Effect "depends on" roomId
// ...
Hereâs how this works:
- You knew
roomId
is a prop, which means it can change over time. - You knew that your Effect reads
roomId
(so its logic depends on a value that may change later). - This is why you specified it as your Effectâs dependency (so that it re-synchronizes when
roomId
changes).
Every time after your component re-renders, React will look at the array of dependencies that you have passed. If any of the values in the array is different from the value at the same spot that you passed during the previous render, React will re-synchronize your Effect. For example, if you passed ["general"]
during the initial render, and later you passed ["travel"]
during the next render, React will compare "general"
and "travel"
. These are different values (compared with Object.is
), so React will re-synchronize your Effect. On the other hand, if your component re-renders but roomId
has not changed, your Effect will remain connected to the same room.
Each Effect represents a separate synchronization process
Resist adding unrelated logic to your Effect only because this logic needs to run at the same time as an Effect you already wrote. For example, letâs say you want to send an analytics event when the user visits the room. You already have an Effect that depends on roomId
, so you might feel tempted to add the analytics call right there:
function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
But imagine you later add another dependency to this Effect that needs to re-establish the connection. If this Effect re-synchronizes, it will also call logVisit(roomId)
for the same room, which you did not intend. Logging the visit is a separate process from connecting. This is why they should be written as two separate Effects:
function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
}, [roomId]);
// ...
}
Each Effect in your code should represent a separate and independent synchronization process.
In the above example, deleting one Effect wouldnât break the other Effectâs logic. This is a good indication that they synchronize different things, and so it made sense to split them up. On the other hand, if you split up a cohesive piece of logic into separate Effects, the code may look âcleanerâ but will be more difficult to maintain. This is why you should think whether the processes are same or separate, not whether the code looks cleaner.
Effects âreactâ to reactive values
Your Effect reads two variables (serverUrl
and roomId
), but you only specified roomId
as a dependency:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
Why doesnât serverUrl
need to be a dependency?
This is because the serverUrl
never changes due to a re-render. Itâs always the same no matter how many times and with which props and state the component re-renders. Since serverUrl
never changes, it wouldnât make sense to specify it as a dependency. After all, dependencies only do something when they change over time!
On the other hand, roomId
may be different on a re-render. Props, state, and other values declared inside the component are reactive because theyâre calculated during rendering and participate in the React data flow.
If serverUrl
was a state variable, it would be reactive. Reactive values must be included in dependencies:
function ChatRoom({ roomId }) { // Props change over time
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // State may change over time
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads props and state
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So you tell React that this Effect "depends on" on props and state
// ...
}
By including serverUrl
as a dependency, you ensure that the Effect re-synchronizes after it changes.
Try changing the selected chat room or edit the server URL in this sandbox:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, [roomId, serverUrl]); return ( <> <label> Server URL:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
Whenever you change a reactive value like roomId
or serverUrl
, the Effect re-connects to the chat server.
What an Effect with empty dependencies means
What happens if you move both serverUrl
and roomId
outside the component?
const serverUrl = 'https://localhost:1234';
const roomId = 'general';
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // â
All dependencies declared
// ...
}
Now your Effectâs code does not use any reactive values, so its dependencies can be empty ([]
).
If you think from the componentâs perspective, the empty []
dependency array means this Effect connects to the chat room only when the component mounts, and disconnects only when the component unmounts. (Keep in mind that React would still re-synchronize it an extra time in development to stress-test your Effectâs logic.)
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; const roomId = 'general'; function ChatRoom() { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, []); return <h1>Welcome to the {roomId} room!</h1>; } export default function App() { const [show, setShow] = useState(false); return ( <> <button onClick={() => setShow(!show)}> {show ? 'Close chat' : 'Open chat'} </button> {show && <hr />} {show && <ChatRoom />} </> ); }
However, if you think from the Effectâs perspective, you donât need to think about mounting and unmounting at all. Whatâs important is youâve specified what your Effect does to start and stop synchronizing. Today, it has no reactive dependencies. But if you ever want the user to change roomId
or serverUrl
over time (and so theyâd have to become reactive), your Effectâs code wonât change. You will only need to add them to the dependencies.
All variables declared in the component body are reactive
Props and state arenât the only reactive values. Values that you calculate from them are also reactive. If the props or state change, your component will re-render, and the values calculated from them will also change. This is why all variables from the component body used by the Effect should also be in the Effect dependency list.
Letâs say that the user can pick a chat server in the dropdown, but they can also configure a default server in settings. Suppose youâve already put the settings state in a context so you read the settings
from that context. Now you calculate the serverUrl
based on the selected server from props and the default server from context:
function ChatRoom({ roomId, selectedServerUrl }) { // roomId is reactive
const settings = useContext(SettingsContext); // settings is reactive
const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads roomId and serverUrl
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So it needs to re-synchronize when either of them changes!
// ...
}
In this example, serverUrl
is not a prop or a state variable. Itâs a regular variable that you calculate during rendering. But itâs calculated during rendering, so it can change due to a re-render. This is why itâs reactive.
All values inside the component (including props, state, and variables in your componentâs body) are reactive. Any reactive value can change on a re-render, so you need to include reactive values as Effectâs dependencies.
In other words, Effects âreactâ to all values from the component body.
Deep Dive
Can global or mutable values be dependencies?
Can global or mutable values be dependencies?
Mutable values (including global variables) arenât reactive.
A mutable value like location.pathname
canât be a dependency. Itâs mutable, so it can change at any time completely outside of the React rendering data flow. Changing it wouldnât trigger a re-render of your component. Therefore, even if you specified it in the dependencies, React wouldnât know to re-synchronize the Effect when it changes. This also breaks the rules of React because reading mutable data during rendering (which is when you calculate the dependencies) breaks purity of rendering. Instead, you should read and subscribe to an external mutable value with useSyncExternalStore
.
A mutable value like ref.current
or things you read from it also canât be a dependency. The ref object returned by useRef
itself can be a dependency, but its current
property is intentionally mutable. It lets you keep track of something without triggering a re-render. But since changing it doesnât trigger a re-render, itâs not a reactive value, and React wonât know to re-run your Effect when it changes.
As youâll learn below on this page, a linter will check for these issues automatically.
React verifies that you specified every reactive value as a dependency
If your linter is configured for React, it will check that every reactive value used by your Effectâs code is declared as its dependency. For example, this is a lint error because both roomId
and serverUrl
are reactive:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { // roomId is reactive const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, []); // <-- Something's wrong here! return ( <> <label> Server URL:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
This may look like a React error, but really React is pointing out a bug in your code. Both roomId
and serverUrl
may change over time, but youâre forgetting to re-synchronize your Effect when they change. As a result, you will remain connected to the initial roomId
and serverUrl
even after the user picks different values in the UI.
To fix the bug, follow the linterâs suggestion to specify roomId
and serverUrl
as dependencies of your Effect:
function ChatRoom({ roomId, serverUrl }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]); // â
All dependencies declared
// ...
}
Try this fix in the sandbox above. Verify that the linter error is gone, and that the chat re-connects when needed.
What to do when you donât want to re-synchronize
In the previous example, youâve fixed the lint error by listing roomId
and serverUrl
as dependencies.
However, you could instead âproveâ to the linter that these values arenât reactive values, i.e. that they canât change as a result of a re-render. For example, if serverUrl
and roomId
donât depend on rendering and always have the same values, you can move them outside the component. Now they donât need to be dependencies:
const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // â
All dependencies declared
// ...
}
You can also move them inside the Effect. They arenât calculated during rendering, so theyâre not reactive:
function ChatRoom() {
useEffect(() => {
const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // â
All dependencies declared
// ...
}
Effects are reactive blocks of code. They re-synchronize when the values you read inside of them change. Unlike event handlers, which only run once per interaction, Effects run whenever synchronization is necessary.
You canât âchooseâ your dependencies. Your dependencies must include every reactive value you read in the Effect. The linter enforces this. Sometimes this may lead to problems like infinite loops and to your Effect re-synchronizing too often. Donât fix these problems by suppressing the linter! Hereâs what to try instead:
-
Check that your Effect represents an independent synchronization process. If your Effect doesnât synchronize anything, it might be unnecessary. If it synchronizes several independent things, split it up.
-
If you want to read the latest value of props or state without âreactingâ to it and re-synchronizing the Effect, you can split your Effect into a reactive part (which youâll keep in the Effect) and a non-reactive part (which youâll extract into something called an Event function). Read more about separating Events from Effects.
-
Avoid relying on objects and functions as dependencies. If you create objects and functions during rendering and then read them from an Effect, they will be different on every render. This will cause your Effect to re-synchronize every time. Read more about removing unnecessary dependencies from your Effects.
Recap
- Components can mount, update, and unmount.
- Each Effect has a separate lifecycle from the surrounding component.
- Each Effect describes a separate synchronization process that can start and stop.
- When you write and read Effects, you should think from each individual Effectâs perspective (how to start and stop synchronization) rather than from the componentâs perspective (how it mounts, updates, or unmounts).
- Values declared inside the component body are âreactiveâ.
- Reactive values should re-synchronize the Effect because they can change over time.
- The linter verifies that all reactive values used inside the Effect are specified as dependencies.
- All errors flagged by the linter are legitimate. Thereâs always a way to fix the code that doesnât break the rules.
Challenge 1 of 5: Fix reconnecting on every keystroke
In this example, the ChatRoom
component connects to the chat room when the component mounts, disconnects when it unmounts, and reconnects when you select a different chat room. This behavior is correct, so you need to keep it working.
However, there is a problem. Whenever you type into the message box input at the bottom, ChatRoom
also reconnects to the chat. (You can notice this by clearing the console and typing into the input.) Fix the issue so that this doesnât happen.
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }); return ( <> <h1>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }