Understanding how the useEffect
Hook works is one of the most important concepts for mastering React today. Suppose you have been working with React for several years. In that case, it is especially crucial to understand how working with useEffect
differs from working with the lifecycle methods of class-based components.
With useEffect
, you invoke side effects from within functional components, which is an important concept to understand in the React Hooks era. Working with the side effects invoked by the useEffect
Hook may seem cumbersome at first, but you’ll eventually everything will make sense.
The goal of this article is to gather information about the underlying concepts of useEffect
and, in addition, to provide learnings from my own experience with the useEffect
Hook. The code snippets provided are part of my companion GitHub project.
The core concepts of useEffect
What are the effects, really? Examples are:
- Fetching data
- Reading from local storage
- Registering and de-registering event listeners
React’s effects are a completely different animal than the lifecycle methods of class-based components. The abstraction level differs, too.
“I’ve found Hooks to be a very powerful abstraction — possibly a little too powerful. As the saying goes, with great power comes great responsibility.”
– Bryan Manuele
To their credit, lifecycle methods do give components a predictable structure. The code is more explicit in contrast to effects, so developers can directly spot the relevant parts (e.g., componentDidMount
) in terms of performing tasks at particular lifecycle phases (e.g., on component unmount).
As we will see later, the useEffect
Hook fosters the separation of concerns and reduces code duplication. For example, the official React docs show that you can avoid the duplicated code that results from lifecycle methods with one useEffect
statement.
The key concepts of using effects
Before we continue, we should summarize the main concepts you’ll need to understand to master useEffect
. Throughout the article, I will highlight the different aspects in great detail:
- You must thoroughly understand when components (re-)render because effects run after every render cycle
- Effects are always executed after rendering, but you can opt-out of this behavior
- You must understand basic JavaScript concepts about values to opt out or skip effects. An effect is only rerun if at least one of the values specified as part of the effect’s dependencies has changed since the last render cycle
- You should ensure that components are not re-rendered unnecessarily. This constitutes another strategy to skip unnecessary reruns of effects.
- You have to understand that functions defined in the body of your function component get recreated on every render cycle. This has an impact if you use it inside of your effect. There are strategies to cope with it (hoist them outside of the component, define them inside of the effect, use
useCallback
) - You have to understand basic JavaScript concepts such as stale closures, otherwise, you might have trouble tackling problems with outdated props or state values inside of your effect. There are strategies to solve this, e.g., with an effect’s dependency array or with the
useRef
Hook - You should not ignore suggestions from the React Hooks ESLint plugin. Do not blindly remove dependencies or carelessly use ESLint’s disable comments; you most likely have introduced a bug. You may still lack understanding of some important concepts
- Do not mimic the lifecycle methods of class-based components. This way of thinking does more harm than good. Instead, think more about data flow and state associated with effects because you run effects based on state changes across render cycles
The following tweet provides an excellent way to think about the last bullet point:
“The question is not ‘when does this effect run,’ the question is ‘with which state does this effect synchronize?’ ”
– Ryan Florence
Using useEffect for asynchronous tasks
For your fellow developers, useEffect
code blocks are clear indicators of asynchronous tasks. Of course, it is possible to write asynchronous code without useEffect
, but it is not the “React way,” and it increases both complexity and the likelihood of introducing errors.
Instead of writing asynchronous code without useEffect
that might block the UI, utilizing useEffect
is a known pattern in the React community — especially the way the React team has designed it to execute side effects.
Another advantage of using useEffect
is that developers can easily overview the code and quickly recognize code that is executed “outside the control flow,” which becomes relevant only after the first render cycle.
On top of that, useEffect
blocks are candidates to extract into reusable and even more semantic custom Hooks.
Using multiple effects to separate concerns
Don’t be afraid to use multiple useEffect
statements in your component. While useEffect
is designed to handle only one concern, you’ll sometimes need more than one effect.
When are effects executed within the component lifecycle?
First, a reminder: don’t think in lifecycle methods anymore! Don’t try to mimic these methods! I will go into more detail about the motives later.
This interactive diagram shows the React phases in which certain lifecycle methods (e.g., componentDidMount
) are executed:
In contrast, the next diagram shows how things work in the context of functional components:
This may sound strange initially, but effects defined with useEffect
are invoked after render. To be more specific, it runs both after the first render and after every update. In contrast to lifecycle methods, effects don’t block the UI because they run asynchronously.
If you are new to React, I would recommend ignoring class-based components and lifecycle methods and, instead, learning how to develop functional components and how to decipher the powerful possibilities of effects. Class-based components are rarely used in more recent React development projects.
If you are a seasoned React developer and are familiar with class-based components, you have to do some of the same things in your projects today as you did a few years ago when there were no Hooks.
For example, it is pretty common to “do something” when the component is first rendered. The difference with Hooks here is subtle: you do not do something after the component is mounted; you do something after the component is first presented to the user. As others have noted, Hooks force you to think more from the user’s perspective.
The useEffect control flow at a glance
This section briefly describes the control flow of effects. The following steps are carried out for a functional React component if at least one effect is defined:
- The component will be re-rendered based on a state, prop, or context change
- If one or more
useEffect
declarations exist for the component, React checks eachuseEffect
to determine whether it fulfills the conditions to execute the implementation (the body of the callback function provided as first argument). In this case, “conditions” mean one or more dependencies have changed since the last render cycle
Dependencies are array items provided as the optional second argument of the useEffect
call. Array values must be from the component scope (i.e., props, state, context, or values derived from the aforementioned):
- After the execution of every effect, scheduling of new effects occurs based on every effect’s dependencies. If an effect does not specify a dependency array at all, it means that this effect is executed after every render cycle
- Cleanup is an optional step for every effect if the body of the
useEffect
callback function (first argument) returns a so-called “cleanup callback function”. In this case, the cleanup function gets invoked before the execution of the effect, beginning with the second scheduling cycle. This also means that if there is no second execution of an effect scheduled, the cleanup function is invoked before the React component gets destroyed.
I am quite sure that this lifecycle won’t be entirely clear to you if you have little experience with effects. That’s why I explain every aspect in great detail throughout this article. I encourage you to return to this section later — I’m sure your next read will be totally clear.
How to execute side effects with useEffect?
The signature of the useEffect
Hook looks like this :
useEffect(() => {}, []);
Because the second argument is optional, the following execution is perfectly fine :
useEffect(() => {});
Let’s take a look at an example. The user can change the document title with an input field :
import React, { useState, useRef, useEffect } from 'react';
function EffectsDemoNoDependency() {
const [title, setTitle] = useState('default title');
const titleRef = useRef();
useEffect(() => {
console.log('useEffect');
document.title = title;
});
const handleClick = () => setTitle(titleRef.current.value);
console.log('render');
return (
<div>
<input ref={titleRef} />
<button onClick={handleClick}>change title</button>
</div>
);
}
The useEffect
statement is only defined with a single, mandatory argument to implement the actual effect to execute. In our case, we use the state variable representing the title and assign its value to document.title
.
Because we skipped the second argument, this useEffect
is called after every render. Because we implemented an uncontrolled input field with the help of the useRef
Hook, handleClick
is only invoked after the user clicks on the button. This causes a re-render because setTitle
performs a state change.
After every render cycle, useEffect
is executed again. To demonstrate this, I added two console.log
statements:
The first two log outputs are due to the initial rendering after the component was mounted. Let’s add another state variable to the example to toggle a dark mode with the help of a checkbox :
function EffectsDemoTwoStates() { const [title, setTitle] = useState('default title'); const titleRef = useRef(); const [darkMode, setDarkMode] = useState(false); useEffect(() => { console.log('useEffect'); document.title = title; }); console.log('render'); const handleClick = () => setTitle(titleRef.current.value); const handleCheckboxChange = () => setDarkMode((prev) => !prev); return ( <div className={darkMode ? 'dark-mode' : ''}> <label htmlFor='darkMode'>dark mode</label> <input name='darkMode' type='checkbox' checked={darkMode} onChange={handleCheckboxChange} /> <input ref={titleRef} /> <button onClick={handleClick}>change title</button> </div> ); }
However, this example leads to unnecessary effects when you toggle the darkMode
state variable :
Of course, it’s not a huge deal in this example, but you can imagine more problematic use cases that cause bugs or, at least, performance issues. Let’s take a look at the following code and try to read the initial title from local storage, if available, in an additional useEffect
block :
function EffectsDemoInfiniteLoop() { const [title, setTitle] = useState('default title'); const titleRef = useRef(); useEffect(() => { console.log('useEffect title'); document.title = title; }); useEffect(() => { console.log('useEffect local storage'); const persistedTitle = localStorage.getItem('title'); setTitle(persistedTitle || []); }); console.log('render'); const handleClick = () => setTitle(titleRef.current.value); return ( <div> <input ref={titleRef} /> <button onClick={handleClick}>change title</button> </div> ); }
As you can see, we have an infinite loop of effects because every state change with setTitle
triggers another effect, which updates the state again :
The importance of the dependency array
Let’s go back to our previous example with two states (title and dark mode). Why do we have the problem of unnecessary effects?
Again, if you do not provide a dependency array, every scheduled useEffect
is executed. This means that after every render cycle, every effect defined in the corresponding component is executed one after the other based on the positioning in the source code.
So the order of your effect definitions matter. In our case, our single useEffect
statement is executed whenever one of the state variables change.
You have the ability to opt out from this behavior. This is managed with dependencies you provide as array entries. In these cases, React only executes the useEffect
statement if at least one of the provided dependencies has changed since the previous run. In other words, with the dependency array, you make the execution dependent on certain conditions.
More often than not, this is what we want; we usually want to execute side effects after specific conditions, e.g., data has changed, a prop changed, or the user first sees our component. Another strategy to skip unnecessary effects is to prevent unnecessary re-renders in the first place with, for example, React.memo
, as we’ll see later.
Back to our example where we want to skip unnecessary effects after an intended re-render. We just have to add an array with title
as a dependency. With that, the effect is only executed when the values between render cycles differ :
useEffect(() => {
console.log('useEffect');
document.title = title;
}, [title]);
Here’s the complete code snippet :
function EffectsDemoTwoStatesWithDependeny() { const [title, setTitle] = useState('default title'); const [darkMode, setDarkMode] = useState(false); const titleRef = useRef(); useEffect(() => { console.log('useEffect'); document.title = title; }, [title]); console.log('render'); const handleClick = () => setTitle(titleRef.current.value); const handleCheckboxChange = () => setDarkMode((prev) => !prev); return ( <div className={darkMode ? 'view dark-mode' : 'view'}> <label htmlFor='darkMode'>dark mode</label> <input name='darkMode' type='checkbox' checked={darkMode} onChange={handleCheckboxChange} /> <input ref={titleRef} /> <button onClick={handleClick}>change title</button> </div> ); }
As you can see in the recording, effects are only invoked as expected on pressing the button :
It’s also possible to add an empty dependency array. In this case, effects are only executed once; it is similar to the componentDidMount()
lifecycle method. To demonstrate this, let’s take a look at the previous example with the infinite loop of effects :
function EffectsDemoEffectOnce() { const [title, setTitle] = useState('default title'); const titleRef = useRef(); useEffect(() => { console.log('useEffect title'); document.title = title; }); useEffect(() => { console.log('useEffect local storage'); const persistedTitle = localStorage.getItem('title'); setTitle(persistedTitle || []); }, []); console.log('render'); const handleClick = () => setTitle(titleRef.current.value); return ( <div> <input ref={titleRef} /> <button onClick={handleClick}>change title</button> </div> ); }
We just added an empty array as our second argument. Because of this, the effect is only executed once after the first render and skipped for the following render cycles :
If you think about it, this behavior makes sense. In principle, the dependency array says, “Execute the effect provided by the first argument after the next render cycle whenever one of the arguments changes.” However, we don’t have any argument, so dependencies will never change in the future.
That’s why using an empty dependency array makes React invoke an effect only once — after the first render. The second render along with the second useEffect title
is due to the state change invoked by setTitle()
after we read the value from local storage.
The rules of Hooks
Before we continue with more examples, we have to talk about the general rules of Hooks. These are not exclusive to the useEffect
Hook, but it’s important to understand at which places in your code you can define effects. You need to follow these rules to use Hooks :
- Hooks can only be invoked from the top-level function constituting your functional React component
- Hooks may not be called from nested code (e.g., loops, conditions, or another function body)
- Custom Hooks are special functions, however, and Hooks may be called from the top-level function of the custom Hook. In addition, rule two is also true
How the React Hooks ESLint plugin promotes understanding of the rules of Hooks?
There’s a handy ESLint plugin that assists you in following the rules of Hooks. It lets you know if you violate one of the rules :
In addition, it helps you to provide a correct dependency array for effects in order to prevent bugs :
This plugin is great because, in practice, you might miss the opportunity to add dependencies to the list; this is not always obvious at firstI like the plugin because its messages foster learning more about how effects work.
If you don’t understand why the plugin wants you to add a specific dependency, please don’t prematurely ignore it! You should at least have an excellent explanation for doing so. I have recently discovered that, in some circumstances, you most likely will have a bug if you omit the dependency
useEffect(() => {}, []);
Finally, be aware that the plugin is not omniscient. You have to accept that the ESLint plugin cannot understand the runtime behavior of your code. It can only apply static code analysis. There are certainly cases where the plugin cannot assist you.
However, I have no arguments against integrating the plugin into your project setup. It reduces error-proneness and increases robustness. In addition, take a closer look at the provided suggestions; they might enable new insights into concepts you haven’t grasped completely.
Understanding how the useEffect
Hook works is one of the most important concepts for mastering React today. Suppose you have been working with React for several years. In that case, it is especially crucial to understand how working with useEffect
differs from working with the lifecycle methods of class-based components.
With useEffect
, you invoke side effects from within functional components, which is an important concept to understand in the React Hooks era. Working with the side effects invoked by the useEffect
Hook may seem cumbersome at first, but you’ll eventually everything will make sense.
The goal of this article is to gather information about the underlying concepts of useEffect
and, in addition, to provide learnings from my own experience with the useEffect
Hook. The code snippets provided are part of my companion GitHub project.