Synchronizing with Effects
Some components need to synchronize with external systems. For example, you might want to control a non-React component based on the React state, set up a server connection, or send an analytics log when a component appears on the screen. Effects let you run some code after rendering so that you can synchronize your component with some system outside of React.
AprenderĂĄs
- What Effects are
- How Effects are different from events
- How to declare an Effect in your component
- How to skip re-running an Effect unnecessarily
- Why Effects run twice in development and how to fix them
What are Effects and how are they different from events?
Before getting to Effects, you need to be familiar with two types of logic inside React components:
-
Rendering code (introduced in Describing the UI) lives at the top level of your component. This is where you take the props and state, transform them, and return the JSX you want to see on the screen. Rendering code must be pure. Like a math formula, it should only calculate the result, but not do anything else.
-
Event handlers (introduced in Adding Interactivity) are nested functions inside your components that do things rather than just calculate them. An event handler might update an input field, submit an HTTP POST request to buy a product, or navigate the user to another screen. Event handlers contain âside effectsâ (they change the programâs state) and are caused by a specific user action (for example, a button click or typing).
Sometimes this isnât enough. Consider a ChatRoom
component that must connect to the chat server whenever itâs visible on the screen. Connecting to a server is not a pure calculation (itâs a side effect) so it canât happen during rendering. However, there is no single particular event like a click that causes ChatRoom
to be displayed.
Effects let you specify side effects that are caused by rendering itself, rather than by a particular event. Sending a message in the chat is an event because it is directly caused by the user clicking a specific button. However, setting up a server connection is an Effect because it needs to happen regardless of which interaction caused the component to appear. Effects run at the end of the rendering process after the screen updates. This is a good time to synchronize the React components with some external system (like network or a third-party library).
You might not need an Effect
Donât rush to add Effects to your components. Keep in mind that Effects are typically used to âstep outâ of your React code and synchronize with some external system. This includes browser APIs, third-party widgets, network, and so on. If your Effect only adjusts some state based on other state, you might not need an Effect.
How to write an Effect
To write an Effect, follow these three steps:
- Declare an Effect. By default, your Effect will run after every render.
- Specify the Effect dependencies. Most Effects should only re-run when needed rather than after every render. For example, a fade-in animation should only trigger when a component appears. Connecting and disconnecting to a chat room should only happen when the component appears and disappears, or when the chat room changes. You will learn how to control this by specifying dependencies.
- Add cleanup if needed. Some Effects need to specify how to stop, undo, or clean up whatever they were doing. For example, âconnectâ needs âdisconnectâ, âsubscribeâ needs âunsubscribeâ, and âfetchâ needs either âcancelâ or âignoreâ. You will learn how to do this by returning a cleanup function.
Letâs look at each of these steps in detail.
Step 1: Declare an Effect
To declare an Effect in your component, import the useEffect
Hook from React:
import { useEffect } from 'react';
Then, call it at the top level of your component and put some code inside your Effect:
function MyComponent() {
useEffect(() => {
// Code here will run after *every* render
});
return <div />;
}
Every time your component renders, React will update the screen and then run the code inside useEffect
. In other words, useEffect
âdelaysâ a piece of code from running until that render is reflected on the screen.
Letâs see how you can use an Effect to synchronize with an external system. Consider a <VideoPlayer>
React component. It would be nice to control whether itâs playing or paused by passing an isPlaying
prop to it:
<VideoPlayer isPlaying={isPlaying} />;
Your custom VideoPlayer
component renders the built-in browser <video>
tag:
function VideoPlayer({ src, isPlaying }) {
// TODO: do something with isPlaying
return <video src={src} />;
}
However, the browser <video>
tag does not have an isPlaying
prop. The only way to control it is to manually call the play()
and pause()
methods on the DOM element. You need to synchronize the value of isPlaying
prop, which tells whether the video should currently be playing, with imperative calls like play()
and pause()
.
Weâll need to first get a ref to the <video>
DOM node.
You might be tempted to try to call play()
or pause()
during rendering, but that isnât correct:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); if (isPlaying) { ref.current.play(); // Calling these while rendering isn't allowed. } else { ref.current.pause(); // Also, this crashes. } return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); return ( <> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
The reason this code isnât correct is that it tries to do something with the DOM node during rendering. In React, rendering should be a pure calculation of JSX and should not contain side effects like modifying the DOM.
Moreover, when VideoPlayer
is called for the first time, its DOM does not exist yet! There isnât a DOM node yet to call play()
or pause()
on, because React doesnât know what DOM to create until after you return the JSX.
The solution here is to wrap the side effect with useEffect
to move it out of the rendering calculation:
import { useEffect, useRef } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}
By wrapping the DOM update in an Effect, you let React update the screen first. Then your Effect runs.
When your VideoPlayer
component renders (either the first time or if it re-renders), a few things will happen. First, React will update the screen, ensuring the <video>
tag is in the DOM with the right props. Then React will run your Effect. Finally, your Effect will call play()
or pause()
depending on the value of isPlaying
prop.
Press Play/Pause multiple times and see how the video player stays synchronized to the isPlaying
value:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { ref.current.play(); } else { ref.current.pause(); } }); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); return ( <> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
In this example, the âexternal systemâ you synchronized to React state was the browser media API. You can use a similar approach to wrap legacy non-React code (like jQuery plugins) into declarative React components.
Note that controlling a video player is much more complex in practice. Calling play()
may fail, the user might play or pause using the built-in browser controls, and so on. This example is very simplified and incomplete.
Step 2: Specify the Effect dependencies
By default, Effects run after every render. Often, this is not what you want:
- Sometimes, itâs slow. Synchronizing with an external system is not always instant, so you might want to skip doing it unless itâs necessary. For example, you donât want to reconnect to the chat server on every keystroke.
- Sometimes, itâs wrong. For example, you donât want to trigger a component fade-in animation on every keystroke. The animation should only play once when the component appears for the first time.
To demonstrate the issue, here is the previous example with a few console.log
calls and a text input that updates the parent componentâs state. Notice how typing causes the Effect to re-run:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { console.log('Calling video.play()'); ref.current.play(); } else { console.log('Calling video.pause()'); ref.current.pause(); } }); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
You can tell React to skip unnecessarily re-running the Effect by specifying an array of dependencies as the second argument to the useEffect
call. Start by adding an empty []
array to the above example on line 14:
useEffect(() => {
// ...
}, []);
You should see an error saying React Hook useEffect has a missing dependency: 'isPlaying'
:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { console.log('Calling video.play()'); ref.current.play(); } else { console.log('Calling video.pause()'); ref.current.pause(); } }, []); // This causes an error return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
The problem is that the code inside of your Effect depends on the isPlaying
prop to decide what to do, but this dependency was not explicitly declared. To fix this issue, add isPlaying
to the dependency array:
useEffect(() => {
if (isPlaying) { // It's used here...
// ...
} else {
// ...
}
}, [isPlaying]); // ...so it must be declared here!
Now all dependencies are declared, so there is no error. Specifying [isPlaying]
as the dependency array tells React that it should skip re-running your Effect if isPlaying
is the same as it was during the previous render. With this change, typing into the input doesnât cause the Effect to re-run, but pressing Play/Pause does:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { console.log('Calling video.play()'); ref.current.play(); } else { console.log('Calling video.pause()'); ref.current.pause(); } }, [isPlaying]); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Pause' : 'Play'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
The dependency array can contain multiple dependencies. React will only skip re-running the Effect if all of the dependencies you specify have exactly the same values as they had during the previous render. React compares the dependency values using the Object.is
comparison. See the useEffect
API reference for more details.
Notice that you canât âchooseâ your dependencies. You will get a lint error if the dependencies you specified donât match what React expects based on the code inside your Effect. This helps catch many bugs in your code. If your Effect uses some value but you donât want to re-run the Effect when it changes, youâll need to edit the Effect code itself to not âneedâ that dependency.
Deep Dive
Why was the ref omitted from the dependency array?
Why was the ref omitted from the dependency array?
This Effect uses both ref
and isPlaying
, but only isPlaying
is declared as a dependency:
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying]);
This is because the ref
object has a stable identity: React guarantees youâll always get the same object from the same useRef
call on every render. It never changes, so it will never by itself cause the Effect to re-run. Therefore, it does not matter whether you include it or not. Including it is fine too:
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying, ref]);
The set
functions returned by useState
also have stable identity, so you will often see them omitted from the dependencies too. If the linter lets you omit a dependency without errors, it is safe to do.
Omitting always-stable dependencies only works when the linter can âseeâ that the object is stable. For example, if ref
was passed from a parent component, you would have to specify it in the dependency array. However, this is good because you canât know whether the parent component always passes the same ref, or passes one of several refs conditionally. So your Effect would depend on which ref is passed.
Step 3: Add cleanup if needed
Consider a different example. Youâre writing a ChatRoom
component that needs to connect to the chat server when it appears. You are given a createConnection()
API that returns an object with connect()
and disconnect()
methods. How do you keep the component connected while it is displayed to the user?
Start by writing the Effect logic:
useEffect(() => {
const connection = createConnection();
connection.connect();
});
It would be slow to connect to the chat after every re-render, so you add the dependency array:
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
The code inside the Effect does not use any props or state, so your dependency array is []
(empty). This tells React to only run this code when the component âmountsâ, i.e. appears on the screen for the first time.
Letâs try running this code:
import { useEffect } from 'react'; import { createConnection } from './chat.js'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(); connection.connect(); }, []); return <h1>Welcome to the chat!</h1>; }
This Effect only runs on mount, so you might expect "â
Connecting..."
to be printed once in the console. However, if you check the console, "â
Connecting..."
gets printed twice. Why does it happen?
Imagine the ChatRoom
component is a part of a larger app with many different screens. The user starts their journey on the ChatRoom
page. The component mounts and calls connection.connect()
. Then imagine the user navigates to another screenâfor example, to the Settings page. The ChatRoom
component unmounts. Finally, the user clicks Back and ChatRoom
mounts again. This would set up a second connectionâbut the first connection was never destroyed! As the user navigates across the app, the connections would keep piling up.
Bugs like this are easy to miss without extensive manual testing. To help you spot them quickly, in development React remounts every component once immediately after its initial mount. Seeing the "â
Connecting..."
log twice helps you notice the real issue: your code doesnât close the connection when the component unmounts.
To fix the issue, return a cleanup function from your Effect:
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);
React will call your cleanup function each time before the Effect runs again, and one final time when the component unmounts (gets removed). Letâs see what happens when the cleanup function is implemented:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(); connection.connect(); return () => connection.disconnect(); }, []); return <h1>Welcome to the chat!</h1>; }
Now you get three console logs in development:
"â Connecting..."
"â Disconnected."
"â Connecting..."
This is the correct behavior in development. By remounting your component, React verifies that navigating away and back would not break your code. Disconnecting and then connecting again is exactly what should happen! When you implement the cleanup well, there should be no user-visible difference between running the Effect once vs running it, cleaning it up, and running it again. Thereâs an extra connect/disconnect call pair because React is probing your code for bugs in development. This is normal and you shouldnât try to make it go away.
In production, you would only see "â
Connecting..."
printed once. Remounting components only happens in development to help you find Effects that need cleanup. You can turn off Strict Mode to opt out of the development behavior, but we recommend keeping it on. This lets you find many bugs like the one above.
How to handle the Effect firing twice in development?
React intentionally remounts your components in development to help you find bugs like in the last example. The right question isnât âhow to run an Effect onceâ, but âhow to fix my Effect so that it works after remountingâ.
Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldnât be able to distinguish between the Effect running once (as in production) and a setup â cleanup â setup sequence (as youâd see in development).
Most of the Effects youâll write will fit into one of the common patterns below.
Controlling non-React widgets
Sometimes you need to add UI widgets that arenât written to React. For example, letâs say youâre adding a map component to your page. It has a setZoomLevel()
method, and youâd like to keep the zoom level in sync with a zoomLevel
state variable in your React code. Your Effect would look like similar to this:
useEffect(() => {
const map = mapRef.current;
map.setZoomLevel(zoomLevel);
}, [zoomLevel]);
Note that there is no cleanup needed in this case. In development, React will call the Effect twice, but this is not a problem because calling setZoomLevel
twice with the same value does not do anything. It may be slightly slower, but this doesnât matter because the remounting is development-only and wonât happen in production.
Some APIs may not allow you to call them twice in a row. For example, the showModal
method of the built-in <dialog>
element throws if you call it twice. Implement the cleanup function and make it close the dialog:
useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
}, []);
In development, your Effect will call showModal()
, then immediately close()
, and then showModal()
again. This has the same user-visible behavior as calling showModal()
once, as you would see in production.
Subscribing to events
If your Effect subscribes to something, the cleanup function should unsubscribe:
useEffect(() => {
function handleScroll(e) {
console.log(e.clientX, e.clientY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
In development, your Effect will call addEventListener()
, then immediately removeEventListener()
, and then addEventListener()
again with the same handler. So there would be only one active subscription at a time. This has the same user-visible behavior as calling addEventListener()
once, as you would see in production.
Triggering animations
If your Effect animates something in, the cleanup function should reset the animation to the initial values:
useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // Trigger the animation
return () => {
node.style.opacity = 0; // Reset to the initial value
};
}, []);
In development, opacity will be set to 1
, then to 0
, and then to 1
again. This should have the same user-visible behavior as setting it to 1
directly, which is what would happen in production. If you use a third-party animation library with support for tweening, your cleanup function should reset the tweenâs timeline to its initial state.
Fetching data
If your Effect fetches something, the cleanup function should either abort the fetch or ignore its result:
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
You canât âundoâ a network request that already happened, but your cleanup function should ensure that the fetch thatâs not relevant anymore does not keep affecting your application. For example, if the userId
changes from 'Alice'
to 'Bob'
, cleanup ensures that the 'Alice'
response is ignored even if it arrives after 'Bob'
.
In development, you will see two fetches in the Network tab. There is nothing wrong with that. With the approach above, the first Effect will immediately get cleaned up so its copy of the ignore
variable will be set to true
. So even though there is an extra request, it wonât affect the state thanks to the if (!ignore)
check.
In production, there will only be one request. If the second request in development is bothering you, the best approach is to use a solution that deduplicates requests and caches their responses between components:
function TodoList() {
const todos = useSomeDataLibrary(`/api/user/${userId}/todos`);
// ...
This will not only improve the development experience, but also make your application feel faster. For example, the user pressing the Back button wonât have to wait for some data to load again because it will be cached. You can either build such a cache yourself or use one of the many existing alternatives to manual fetching in Effects.
Deep Dive
What are good alternatives to data fetching in Effects?
What are good alternatives to data fetching in Effects?
Writing fetch
calls inside Effects is a popular way to fetch data, especially in fully client-side apps. This is, however, a very manual approach and it has significant downsides:
- Effects donât run on the server. This means that the initial server-rendered HTML will only include a loading state with no data. The client computer will have to download all JavaScript and render your app only to discover that now it needs to load the data. This is not very efficient.
- Fetching directly in Effects makes it easy to create ânetwork waterfallsâ. You render the parent component, it fetches some data, renders the child components, and then they start fetching their data. If the network is not very fast, this is significantly slower than fetching all data in parallel.
- Fetching directly in Effects usually means you donât preload or cache data. For example, if the component unmounts and then mounts again, it would have to fetch the data again.
- Itâs not very ergonomic. Thereâs quite a bit of boilerplate code involved when writing
fetch
calls in a way that doesnât suffer from bugs like race conditions.
This list of downsides is not specific to React. It applies to fetching data on mount with any library. Like with routing, data fetching is not trivial to do well, so we recommend the following approaches:
- If you use a framework, use its built-in data fetching mechanism. Modern React frameworks have integrated data fetching mechanisms that are efficient and donât suffer from the above pitfalls.
- Otherwise, consider using or building a client-side cache. Popular open source solutions include React Query, useSWR, and React Router 6.4+. You can build your own solution too, in which case you would use Effects under the hood but also add logic for deduplicating requests, caching responses, and avoiding network waterfalls (by preloading data or hoisting data requirements to routes).
You can continue fetching data directly in Effects if neither of these approaches suit you.
Sending analytics
Consider this code that sends an analytics event on the page visit:
useEffect(() => {
logVisit(url); // Sends a POST request
}, [url]);
In development, logVisit
will be called twice for every URL, so you might be tempted to try to work around it. We recommend to keep this code as is. Like with earlier examples, there is no user-visible behavior difference between running it once and running it twice. From a practical point of view, logVisit
should not do anything in development because you donât want the logs from the development machines to skew the production metrics. Your component remounts every time you save its file, so it would send extra visits during development anyway.
In production, there will be no duplicate visit logs.
To debug the analytics events youâre sending, you can deploy your app to a staging environment (which runs in production mode) or temporarily opt out of Strict Mode and its development-only remounting checks. You may also send analytics from the route change event handlers instead of Effects. For even more precise analytics, intersection observers can help track which components are in the viewport and how long they remain visible.
Not an Effect: Initializing the application
Some logic should only run once when the application starts. You can put it outside your components:
if (typeof window !== 'undefined') { // Check if we're running in the browser.
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
}
This guarantees that such logic only runs once after the browser loads the page.
Not an Effect: Buying a product
Sometimes, even if you write a cleanup function, thereâs no way to prevent user-visible consequences of running the Effect twice. For example, maybe your Effect sends a POST request like buying a product:
useEffect(() => {
// đŽ Wrong: This Effect fires twice in development, exposing a problem in the code.
fetch('/api/buy', { method: 'POST' });
}, []);
You wouldnât want to buy the product twice. However, this is also why you shouldnât put this logic in an Effect. What if the user goes to another page and then presses Back? Your Effect would run again. You donât want to buy the product when the user visits a page; you want to buy it when the user clicks the Buy button.
Buying is not caused by rendering; itâs caused by a specific interaction. It only runs once because the interaction (a click) happens once. Delete the Effect and move your /api/buy
request into the Buy button event handler:
function handleClick() {
// â
Buying is an event because it is caused by a particular interaction.
fetch('/api/buy', { method: 'POST' });
}
This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs. From the userâs perspective, visiting a page shouldnât be different from visiting it, clicking a link, and then pressing Back. React verifies that your components donât break this principle by remounting them once in development.
Putting it all together
This playground can help you âget a feelâ for how Effects work in practice.
This example uses setTimeout
to schedule a console log with the input text to appear three seconds after the Effect runs. The cleanup function cancels the pending timeout. Start by pressing âMount the componentâ:
import { useState, useEffect } from 'react'; function Playground() { const [text, setText] = useState('a'); useEffect(() => { function onTimeout() { console.log('â° ' + text); } console.log('đ” Schedule "' + text + '" log'); const timeoutId = setTimeout(onTimeout, 3000); return () => { console.log('đĄ Cancel "' + text + '" log'); clearTimeout(timeoutId); }; }, [text]); return ( <> <label> What to log:{' '} <input value={text} onChange={e => setText(e.target.value)} /> </label> <h1>{text}</h1> </> ); } export default function App() { const [show, setShow] = useState(false); return ( <> <button onClick={() => setShow(!show)}> {show ? 'Unmount' : 'Mount'} the component </button> {show && <hr />} {show && <Playground />} </> ); }
You will see three logs at first: Schedule "a" log
, Cancel "a" log
, and Schedule "a" log
again. Three second later there will also be a log saying a
. As you learned earlier on this page, the extra schedule/cancel pair is because React remounts the component once in development to verify that youâve implemented cleanup well.
Now edit the input to say abc
. If you do it fast enough, youâll see Schedule "ab" log
immediately followed by Cancel "ab" log
and Schedule "abc" log
. React always cleans up the previous renderâs Effect before the next renderâs Effect. This is why even if you type into the input fast, there is at most one timeout scheduled at a time. Edit the input a few times and watch the console to get a feel for how Effects get cleaned up.
Type something into the input and then immediately press âUnmount the componentâ. Notice how unmounting cleans up the last renderâs Effect. In this example, it clears the last timeout before it has a chance to fire.
Finally, edit the component above and comment out the cleanup function so that the timeouts donât get cancelled. Try typing abcde
fast. What do you expect to happen in three seconds? Will console.log(text)
inside the timeout print the latest text
and produce five abcde
logs? Give it a try to check your intution!
Three seconds later, you should see a sequence of logs (a
, ab
, abc
, abcd
, and abcde
) rather than five abcde
logs. Each Effect âcapturesâ the text
value from its corresponding render. It doesnât matter that the text
state changed: an Effect from the render with text = 'ab'
will always see 'ab'
. In other words, Effects from each render are isolated from each other. If youâre curious how this works, you can read about closures.
Deep Dive
Each render has its own Effects
Each render has its own Effects
You can think of useEffect
as âattachingâ a piece of behavior to the render output. Consider this Effect:
export default function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return <h1>Welcome to {roomId}!</h1>;
}
Letâs see what exactly happens as the user navigates around the app.
Initial render
The user visits <ChatRoom roomId="general" />
. Letâs mentally substitute roomId
with 'general'
:
// JSX for the first render (roomId = "general")
return <h1>Welcome to general!</h1>;
The Effect is also a part of the rendering output. The first renderâs Effect becomes:
// Effect for the first render (roomId = "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
// Dependencies for the first render (roomId = "general")
['general']
React runs this Effect, which connects to the 'general'
chat room.
Re-render with same dependencies
Letâs say <ChatRoom roomId="general" />
re-renders. The JSX output is the same:
// JSX for the second render (roomId = "general")
return <h1>Welcome to general!</h1>;
React sees that the rendering output has not changed, so it doesnât update the DOM.
The Effect from the second render looks like this:
// Effect for the second render (roomId = "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
// Dependencies for the second render (roomId = "general")
['general']
React compares ['general']
from the second render with ['general']
from the first render. Because all dependencies are the same, React ignores the Effect from the second render. It never gets called.
Re-render with different dependencies
Then, the user visits <ChatRoom roomId="travel" />
. This time, the component returns different JSX:
// JSX for the third render (roomId = "travel")
return <h1>Welcome to travel!</h1>;
React updates the DOM to change "Welcome to general"
into "Welcome to travel"
.
The Effect from the third render looks like this:
// Effect for the third render (roomId = "travel")
() => {
const connection = createConnection('travel');
connection.connect();
return () => connection.disconnect();
},
// Dependencies for the third render (roomId = "travel")
['travel']
React compares ['travel']
from the third render with ['general']
from the second render. One dependency is different: Object.is('travel', 'general')
is false
. The Effect canât be skipped.
Before React can apply the Effect from the third render, it needs to clean up the last Effect that did run. The second renderâs Effect was skipped, so React needs to clean up the first renderâs Effect. If you scroll up to the first render, youâll see that its cleanup calls disconnect()
on the connection that was created with createConnection('general')
. This disconnects the app from the 'general'
chat room.
After that, React runs the third renderâs Effect. It connects to the 'travel'
chat room.
Unmount
Finally, letâs say the user navigates away, and the ChatRoom
component unmounts. React runs the last Effectâs cleanup function. The last Effect was from the third render. The third renderâs cleanup destroys the createConnection('travel')
connection. So the app disconnects from the 'travel'
room.
Development-only behaviors
When Strict Mode is on, React remounts every component once after mount (state and DOM are preserved). This helps you find Effects that need cleanup and exposes bugs like race conditions early. Additionally, React will remount the Effects whenever you save a file in development. Both of these behaviors are development-only.
RecapitulaciĂłn
- Unlike events, Effects are caused by rendering itself rather than a particular interaction.
- Effects let you synchronize a component with some external system (third-party API, network, etc).
- By default, Effects run after every render (including the initial one).
- React will skip the Effect if all of its dependencies have the same values as during the last render.
- You canât âchooseâ your dependencies. They are determined by the code inside the Effect.
- An empty dependency array (
[]
) corresponds to the component âmountingâ, i.e. being added to the screen. - When Strict Mode is on, React mounts components twice (in development only!) to stress-test your Effects.
- If your Effect breaks because of remounting, you need to implement a cleanup function.
- React will call your cleanup function before the Effect runs next time, and during the unmount.
DesafĂo 1 de 4: Focus a field on mount
In this example, the form renders a <MyInput />
component.
Use the inputâs focus()
method to make MyInput
automatically focus when it appears on the screen. There is already a commented out implementation, but it doesnât quite work. Figure out why it doesnât work, and fix it. (If youâre familiar with the autoFocus
attribute, pretend that it does not exist: we are reimplementing the same functionality from scratch.)
import { useEffect, useRef } from 'react'; export default function MyInput({ value, onChange }) { const ref = useRef(null); // TODO: This doesn't quite work. Fix it. // ref.current.focus() return ( <input ref={ref} value={value} onChange={onChange} /> ); }
To verify that your solution works, press âShow formâ and verify that the input receives focus (becomes highlighted and the cursor is placed inside). Press âHide formâ and âShow formâ again. Verify the input is highlighted again.
MyInput
should only focus on mount rather than after every render. To verify that the behavior is right, press âShow formâ and then repeatedly press the âMake it uppercaseâ checkbox. Clicking the checkbox should not focus the input above it.