Introduction
In the React Components chapter, we learnt that all the logic inside components in React is divided into three categories: rendering, events and effects. Rendering contains all the logic related to the rendering of elements, while events account for all event handler functions.
So where and what are effects then?
Well, that's what this chapter is all about. In this chapter, we shall understand what exactly are effects in React and how to use them to perform side effects in our components.
We'll explore the useEffect()
hook, and see how it really works. We'll also take a look over useEffect()
's dependency array and the idea of the cleanup function to call when the underlying component is cleared up.
The useEffect()
hook represents an extremely useful and crucial feature to understand in React, and so is this chapter. So without further ado, let's begin discovering effects.
What are effects?
As mentioned before, based on the design ideology of React, it's not desirable to perform side effects directly in components. We either do that in event handlers or we do it in the form of effects.
Now before we understand what effects are, there's a good reason to ask why do we even need them. Why can't call side effects be performed from within event handlers in React?
Well, it's not required to look very far off to find an answer to this.
Suppose we want to set up a 3s timer the moment a component is rendered on the document. As you can clearly reason, there's no single event that represents the 'rendering' action, and so we can't perform this task using any event handler whatsoever. This is essentially where effects come in.
Coming back to the definition of effects:
To be more specific, an effect gets executed right after the browser repaint routine (which happens after the reflow).
Because an effect occurs right after the repaint, it has access to all the latest pieces of data, which includes the underlying component's state, refs; and also includes the properties of the DOM.
react.dev puts it this way:
'Connecting to' and 'synchronizing with external systems' refers to the notion of performing side effects, such as sending HTTP requests, or directly mutating the DOM, or working with code outside React.
The term 'Effects' in React's documentation
The latest documentation of React, that explains this concept in the Synchronizing with Effects page, uses the term 'Effects', with the capitalization, to refer to the concept as a distinct one from the more general idea of 'side effects' in React.
We don't use this capitalization to keep the text simple and prevent any ambiguity from arising in case we somehow forget to capitalize the word 'effects'.
It's important to remember that the term 'effects' in this chapter refers to side effects tied to the rendering of a component, whereas the term 'side effects' refers to the more general idea, which also takes into account event handlers.
Now the question is: how to set up effects in React?
Well, that's done using the useEffect()
hook.
The useEffect()
hook
The useEffect()
hook is used to create an effect in React. Perhaps, after useState()
, the second-most used hook in React is useEffect()
— it's that useful!
The way useEffect()
works is pretty basic. Here's its signature:
useEffect(callback, dependencies)
The first callback
argument specifies a callback function to execute after the underlying component is done rendering. As stated before, this callback function executes after the browser repaint routine.
The second optional dependencies
argument specifies the dependency list for the effect. We'll explore this in more detail later on in this chapter.
For now, let's consider an example of a React effect.
In the following code, we set up a 3s timer the moment the App
component is rendered. Once this timer completes, an alert is made saying 'Hello' to the user:
import React, { useEffect } from 'react';
function App() {
useEffect(() => {
setTimeout(() => {
alert('Hello from useEffect()!');
}, 3000);
});
return (
<h1>Wait for 3s for the greeting.</h1>
);
}
Let's consider another example:
In the following example, we bring back the fetch()
call from the React Events chapter, this time as a side effect in useEffect()
instead of as a side effect in an event handler:
import React, { useState, useEffect } from 'react';
function App() {
const [info, setInfo] = useState();
useEffect(() => {
(async () => {
const data = await fetch('https://api.github.com/users/codeguage-code');
setInfo(await data.json());
})();
})
return (
{info ? (
<div className="info">
<p>Name: {info.name}</p>
<p>Bio: {info.bio}</p>
</div>
) : (
<p>Loading...</p>
)}
);
}
There's an important thing to note here: an async
function is nested inside the callback to useEffect()
instead of the callback itself being an async
function.
This is due to the nature of the useEffect()
hook, i.e. it expects the return value of the callback function to be another function; when we provide an async
function as the callback function, as you may know, JavaScript implicitly returns a promise from the function, and this interferes with React's expectation of a function returned from the callback.
Likewise, if we want to use an async
function inside useEffect()
's callback, we have to nest it as a separate function inside the callback and then invoke it.
One simple way to do so is to create the async
function as an IIFE, as we did above.
Let's consider a third example, this time setting up a keydown
event handler on window
in order to showcase the name of the key pressed using the code
property of the fired event.
Here's the JavaScript code:
import React, { useEffect, useState } from 'react';
function App() {
const [key, setKey] = useState();
useEffect(() => {
window.addEventListener('keydown', (e) => {
setKey(e.code);
});
});
return (
key ? (
<div class="key">{key}</div>
) : (
<div class="light">No key entered</div>
)
);
}
And here's the CSS to add some nice styles to the program:
body {
text-align: center;
font-size: 40px;
font-family: sans-serif;
margin: 50px
}
.light {
opacity: 0.4;
}
.key {
font-weight: 600;
font-family: monospace;
}
Initially, as the program loads, the value of key
is undefined
, and, likewise, the .light
element gets rendered. But as soon as a key is pressed, the keydown
event fires, and causes the key
state value to be updated. Thereafter, the App
component renders a .key
element, containing the name of the key pressed.
Pretty amazing!
Now one thing worthwhile discussing at this stage is that, in all of the examples shown thus far, note that the callback provided to useEffect()
gets executed each time the underlying component gets rendered.
As you may agree, this can be problematic, especially if the callback performs a side effect that must be executed only once.
For instance, the third example above calls addEventListener()
to register a keydown
event handler on window
. If the useEffect()
callback here executes again, we'd get another such event handler registered on window
, which is contrary to what we want.
Let's add a simple console.log()
statement inside the handler of keydown
above to see when and how many times does the handler get invoked:
import React, { useEffect, useState } from 'react';
function App() {
const [key, setKey] = useState();
useEffect(() => {
window.addEventListener('keydown', (e) => {
console.log('keydown listener registered');
setKey(e.code);
});
});
return (
key ? (
<div class="key">{key}</div>
) : (
<div class="light">No key entered</div>
)
);
}
Now, if we try pressing a key for the very first time after the program loads, we get one console log. So far, so good.
But as soon as we press another key, we get two further logs, NOT one:
And if we press yet another key, we get three further logs:
Clearly, each time a different key is pressed, the number of logs is one greater than the number of logs made by the previous key press.
But why?
Well, the reason is simple — useEffect()
's callback executes each time App
gets re-rendered by virtue of a different key being pressed, and that effectively registers a whole new keydown
listener on window
, with the previous one(s) being intact.
So the second time when we press a key, we effectively get two different keydown
handlers executed; the third time, we get three different handlers executed; and so on and so forth.
To solve this issue, we just have to prevent the callback function from executing again and again with each re-render.
And in order to do so, we ought to understand the second argument provided to useEffect()
, i.e. the dependency array. Let's see what exactly is it.
The dependency array
As the name suggests, the dependency array of useEffect()
specifies a list of dependencies for the effect.
These dependencies govern when exactly would the effect be put into execution. Without a dependency list, as we saw above, the callback function gets invoked every single time with a new re-render, as we just saw above.
An empty dependency array
However, with an empty array, it gets called only once. After that, the callback never gets called again, thanks to the empty dependency array.
In the following example, we rewrite the previous code where we defined a keydown
event handler on window
, passing an empty dependency list to useEffect()
:
import React, { useEffect, useState } from 'react';
function App() {
const [key, setKey] = useState();
useEffect(() => {
window.addEventListener('keydown', (e) => {
console.log('keydown listener registered');
setKey(e.code);
});
}, []);
return (
key ? (
<div class="key">{key}</div>
) : (
<div class="light">No key entered</div>
)
);
}
As the dependency array is present but empty here, the given useEffect()
callback won't get called after the first render of App
. Thus, we can be rest assured that window
has only one registered keydown
handler at any point in time, and thus each key press would make just a single log.
In the link above, as before, try pressing three different keys and then witnessing the logs made with each one.
The dependency array is an amazing idea!
A dependency array with an item
Remember, however, that the dependency array doesn't always have to be empty — we could populate it with given values as well. In particular, we need to put those values in it that must change in order to mandate an execution of the provided callback function.
Let's consider an example.
Suppose we have a text input element, with an initial value to begin with. The input element is disabled; its value can only be modified by clicking on an Edit button. This button serves to put the whole input container in 'edit' mode; once in this mode, the Edit button gets replaced by a Save button that serves to change the container into 'disabled' mode.
The most important thing to note is that when the edit button is clicked, the input element is focused automatically. The user doesn't have to manually click the input in order to focus it; it's already focused.
Using useEffect()
, useRef()
and useState()
, and a couple other concepts which we've learnt thus far in this course, this program can be brought to life with under 100 lines of code.
Here is the code:
function InputForm({initialValue}) {
const [value, setValue] = useState(initialValue);
const [isEditing, setIsEditing] = useState(false);
const inputElement = useRef();
useEffect(() => {
if (isEditing) {
// Focus the <input> element when we are in 'editing' mode.
inputElement.current.focus();
}
}, [isEditing]);
function onEdit() {
setIsEditing(true);
}
function onSave() {
setIsEditing(false);
}
return (
<>
<input type="text" value={value}
onChange={e => setValue(e.target.value)}
disabled={!isEditing}
ref={inputElement} />
{isEditing ? (
<button onClick={onSave}>Save</button>
) : (
<button onClick={onEdit}>Edit</button>
)}
</>
);
}
function App() {
return (
<InputForm initialValue="React" />
);
}
Carefully follow through the program — it's really simple to understand.
Understanding the code above
The InputForm
component represents the whole 'input container' that we were talking about above. It gets an initial value to render in the <input>
element, in the form of the initialValue
prop.
initialValue
is used to initialize the value
state. This value
state, as is customary in React programs, is used to set the value
attribute of the <input>
element. The onChange
handler of the element updates this value via setValue()
.
The other isEditing
state is used to indicate the mode of InputForm
. If it is true
, it means that we are in 'editing' mode and thus the input must not be disabled; otherwise, we are in 'disabled' mode and so, the input must be disabled.
As can be seen in the code, the value of isEditing
is used to conditionally set the disabled
attribute on the <input>
element, as well as to conditionally render the appropriate kind of <button>
.
The Edit button changes isEditing
to true
with the help of the onEdit()
handler function. The Save button, on the other hand, changes isEditing
to false
with the help of the onSave()
handler. We've explicitly created named function handlers to help better illustrate the role of each button — you could also create the handlers as inline arrow functions.
Whenever isEditing
changes, useEffect()
's callback gets executed. If isEditing
turns out to be true
at this point, the <input>
element is focused, with the help of the DOM focus()
method and the inputElement
ref.
And that's it.
By virtue of the [isEditing]
dependency array passed to useEffect()
, its callback only executes when isEditing
changes.
- If we don't provide a dependency array at all, each time
InputForm
gets re-rendered, our callback would execute. This would mean that as we type into the input field, the callback would execute again and again, which is pointless. - If we provide an empty dependency array, our callback would execute only once and that on the very first render of
InputForm
. This, also, would be pointless.
Surely, the dependency array is amazing!
This program demonstrates the true potential of React even when one just knows how to work with the useState()
, useRef()
and useEffect()
hooks; let alone the case when one is experienced with all of them!
Moving on, the dependency array doesn't just have to contain one dependency. We can have as many of them as we want to.
In this case, the callback would get executed whenever either of the given dependencies changes. In other words, it's not required for all of the dependencies to change in order to necessitate an execution of useEffect()
's callback.
The cleanup function
Let's now talk about the cleanup function.
The callback function provided to useEffect()
can optionally return back a function that gets executed when the underlying component instance is about to be deleted. This function is referred to as the cleanup function.
The cleanup function is called whenever:
- The underlying component instance is thrown away by React.
- The component instance is re-rendered and the effect is put into execution.
The latter is simple to understand: if the effect doesn't get executed on a re-render, then the cleanup function won't get called.
The cleanup function is also of particular significance as is the dependency array when working with useEffect()
. It's what makes sure that we clean up the memory consumed as a result of any code executed as part of the underlying side effect.
Some typical scenarios where useEffect()
's cleanup function is used include when we want to clear up JavaScript timers, e.g. setInterval()
, and when we want to remove a registered event handler.
Let's take an example.
In the following code, we have a RunningCounter
component alongside a normal button that toggles the rendering of RunningCounter
, i.e. if the component is already rendered, it's removed; otherwise, it's rendered:
function RunningCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timerId = setInterval(() => {
console.log('Incremented');
setCount(count => count + 1);
}, 1000);
}, []);
return (
<h1>{count}</h1>
);
}
function App() {
const [counterRendered, setCounterRendered] = useState(true);
return (
<>
{counterRendered && <RunningCounter/>}
<button onClick={() => setCounterRendered(!counterRendered)}>Toggle render</button>
</>
);
}
Notice how RunningCounter
makes a log in setInterval()
.
As <RunningCounter>
gets rendered with the initial page load, we witness a log with each passing second. However, when we press the toggle button, and as a result get the <RunningCounter>
element thrown away, the console logs persist.
This is undesirable behavior.
Worse yet, when we press the toggle button again in order to re-render <RunningCounter>
, we get a new timer set up in addition to the old one. This keeps on happening as we keep on toggling the rendering of <RunningCounter>
.
When RunningCounter
is gone, so should the timer tied to it, right?
But how to do so? Well, this is the job of the cleanup function of useEffect()
.
The idea is simple: when RunningCounter
is thrown away, the ongoing interval must be cleared up as well, using JavaScript's clearInterval()
function.
The following code accomplishes this idea:
function RunningCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
console.log('Incremented');
setCount(count => count + 1);
}, 1000);
// The cleanup function to clear the ongoing interval.
return () => {
clearInterval(timerId);
}
}, []);
return (
<h1>{count}</h1>
);
}
function App() {
const [counterRendered, setCounterRendered] = useState(true);
return (
<>
{counterRendered && <RunningCounter/>}
<button onClick={() => setCounterRendered(!counterRendered)}>Toggle render</button>
</>
);
}
Now, when we press the toggle button, as before, the <RunningCounter>
element gets thrown away, and so does the timer associated with it. Thus, we don't see any logs as the element is gone.
Perfect!