Editor’s note: This article was last updated on 9 February 2023. For more information on React Hooks, check out this cheat sheet.
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.
Fully understanding effects is a complex issue. According to Dan Abramov of the React team, you might have to unlearn some things to fully grasp effects.
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.
Jump ahead:
- The core concepts of
useEffect
- The key concepts of using effects
- Using
useEffect
for asynchronous tasks - Using multiple effects to separate concerns
- When are effects executed within the component lifecycle?
- The
useEffect
control flow at a glance - How to execute side effects with
useEffect
- The importance of the dependency array
- The rules of Hooks
- What are dependency array items?
- Utilizing cleanup functions
- Implications of prop and state changes
- More on prop changes and using the
useCallback
Hook useCallback
withuseContext
useEffect
inside of custom Hooks- Additional thoughts on functions used inside of effects
- Using async functions inside of
useEffect
- Unit testing of effects
- Some more
useEffect
receipts - When not to use
useEffect
The core concepts of useEffect
What are the effects, really? Examples are:
- Fetching data
- Reading from local storage
- Registering and deregistering 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 you try to use only one effect for multiple purposes, it decreases the readability of your code, and some use cases are not realizable.
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( () => { // execute side effect }, // optional dependency array [ // 0 or more entries ] )
Because the second argument is optional, the following execution is perfectly fine:
useEffect(() => { // execute side effect })
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 titleRef = useRef(); const [darkMode, setDarkMode] = useState(false); 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 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(() => { // ... // eslint-disable-next-line react-hooks/exhaustive-deps }, []);
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.
That said, you shouldn’t be as dogmatic as always to satisfy the plugin. Check out the setup in the companion project for this article.
What are dependency array items?
This brings us to an important question: What items should be included in the dependency array? According to the React docs, you must include all values from the component scope that change their values between re-renders.
What does this mean, exactly? All external values referenced inside of the useEffect
callback function, such as props, state variables, or context variables, are dependencies of the effect. Ref containers (i.e., what you directly get from useRef()
and not the current
property) are also valid dependencies. Even local variables, which are derived from the aforementioned values, have to be listed in the dependency array.
It is essential to understand the conceptual thinking of effects; the React team wants you to treat every value used inside of the effect as dynamic. So even if you use a non-function value inside the effect and are pretty sure this value is unlikely to change, you should include the value in the dependency array.
Therefore, make sure to add every value from the component scope to the list of dependencies because you should treat every value as mutable. Remember that if at least one of the dependencies in the array is different from the previous render, the effect will be rerun.
Utilizing cleanup functions
The next snippet shows an example to demonstrate a problematic issue:
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const interval = setInterval(function () { setCount((prev) => prev + 1); }, 1000); }, []); return <p>and the counter counts {count}</p>; } function EffectsDemoUnmount() { const [unmount, setUnmount] = useState(false); const renderDemo = () => !unmount && <Counter />; return ( <div> <button onClick={() => setUnmount(true)}>Unmount child component</button> {renderDemo()} </div> ); }
This code implements a React component representing a counter that increases a number every second. The parent component renders the counter and allows you to destroy the counter by clicking on a button. Take a look at the recording to see what happens when a user clicks on that button:
The child component has registered an interval that invokes a function every second. However, the component was destroyed without unregistering the interval. After the component is destroyed, the interval is still active and wants to update the component’s state variable (count
), which no longer exists.
The solution is to unregister the interval right before unmounting. This is possible with a cleanup function. Therefore, you must return a callback function inside the effect’s callback body:
useEffect(() => { const interval = setInterval(function () { setCount((prev) => prev + 1); }, 1000); // return optional function for cleanup // in this case acts like componentWillUnmount return () => clearInterval(interval); }, []);
I want to emphasize that cleanup functions are not only invoked before destroying the React component. An effect’s cleanup function gets invoked every time right before the execution of the next scheduled effect.
Let’s take a closer look at our example. We used a trick to have an empty dependency array in the first place, so the cleanup function acts like a componentWillUnmount()
lifecycle method. If we do not call setCount
with a callback function that gets the previous value as an argument, we need to come up with the following code, wherein we add a count
to the dependencies array:
useEffect(() => { console.log("useEffect") const interval = setInterval(function () { setCount(count + 1); }, 1000); // return optional function for cleanup // in this case, this cleanup fn is called every time count changes return () => { console.log("cleanup"); clearInterval(interval); } }, [count]);
In comparison, the former example executes the cleanup function only once — on the mount — because we directly prevented using the state variable (count ):
useEffect(() => { console.log("useEffect") const interval = setInterval(function () { setCount(prev => prev + 1); }, 1000); // return optional function for cleanup // in this case, this cleanup fn is called every time count changes return () => { console.log("cleanup"); clearInterval(interval); } }, []);
In this context, the latter approach is a small performance optimization because we reduce the number of cleanup function calls.
Implications of prop and state changes
There is a natural correlation between prop changes and the execution of effects because they cause re-renders, and as we already know, effects are scheduled after every render cycle.
Consider the following example. The plan is that the Counter
component’s interval can be configured by a prop with the same name.
function Counter({ interval }) { const [count, setCount] = useState(0); useEffect(() => { const counterInterval = setInterval(function () { setCount((prev) => prev + 1); }, interval); return () => clearInterval(counterInterval); }, []); return <p>and the counter counts {count}</p>; } function EffectsDemoProps() { const [interval, setInterval] = useState(1000); return ( <div> <input type="text" value={interval} onChange={(evt) => setInterval(evt.target.value)} /> <Counter interval={interval} /> </div> ); }
The handy ESLint plugin points out that we are missing something important: because we haven’t added the interval
prop to the dependency array (having instead defined an empty array), the change to the input field in the parent component is without effect. The initial value of 1000
is used even after we adjust the input field’s value:
Instead, we have to add the prop to the dependency array:
useEffect(() => { const counterInterval = setInterval(function () { setCount((prev) => prev + 1); }, interval); return () => clearInterval(counterInterval); }, [interval]);
Now things look much better:
More on prop changes and using the useCallback
Hook
Let’s extend the example a bit to demonstrate more key concepts in conjunction with prop changes:
const Counter = ({ interval, onDarkModeChange }) => { console.log("render Counter"); const [count, setCount] = useState(0); useEffect(() => { console.log(`useEffect ${onDarkModeChange()}`); const counterInterval = setInterval(function () { setCount((prev) => prev + 1); }, interval); return () => clearInterval(counterInterval); }, [interval, onDarkModeChange]); return <p>and the counter counts {count}</p>; }; const IntervalConfig = ({ onDarkModeChange }) => { console.log("render IntervalConfig"); const [interval, setInterval] = useState(1000); const onChange = (evt) => setInterval(evt.target.value); return ( <div> <input type="text" value={interval} onChange={onChange} /> <Counter interval={interval} onDarkModeChange={onDarkModeChange} /> </div> ); }; const EffectsDemoProps = () => { console.log("render EffectsDemoProps"); const [numberClicks, setNumberClicks] = useState(0); const [darkMode, setDarkMode] = useState(false); const onDarkModeChange = () => (darkMode ? "🌙" : "🌞"); return ( <div style={ darkMode ? { backgroundColor: "black", color: "white" } : { backgroundColor: "white", color: "black" } } > <label htmlFor="darkMode">dark mode</label> <input name="darkMode" type="checkbox" checked={darkMode} onChange={() => setDarkMode((prev) => !prev)} /> <p> <button onClick={() => setNumberClicks((prev) => prev + 1)}> click </button> <span> Number clicks: {numberClicks}</span> </p> <IntervalConfig onDarkModeChange={onDarkModeChange} /> </div> ); };
I added log statements to indicate all component renderings and invocation of our useEffect
statement. Let’s take a look at what happens:
So far, so good — we can toggle the dark mode checkbox, and the effect should be executed, too. The callback function to be executed, onDarkModeChange
, is passed down the component tree to the Counter
component. We added it to the dependency array of the useEffect
statement as suggested by the ESLint plugin:
useEffect(() => { console.log(`useEffect ${onDarkModeChange()}`); const counterInterval = setInterval(function () { setCount((prev) => prev + 1); }, interval); return () => clearInterval(counterInterval); }, [interval, onDarkModeChange]);
As you can see from the recording, the effect is executed if one of the two props, interval
or onDarkModeChange
, changes.
All good? Not so fast — as you can see from the next recording, the effect is mistakenly executed if we click on the button:
Sure, the state of the EffectsDemoProps
changes, and this component is rendered along with its child components. The solution is to use React.memo
, right?
const Counter = React.memo(({ interval, onDarkModeChange }) => { // ... }); const IntervalConfig = React.memo(({ onDarkModeChange }) => { // ... });
The components are rendered, and the effect is still mistakenly executed:
Why is our Counter
component’s effect executed? The problem lies in the onDarkModeChange
function:
const EffectsDemoProps = () => { // ... const onDarkModeChange = () => (darkMode ? "🌙" : "🌞"); // ... };
On button click, the numberClicks
state of the EffectsDemoProps
component gets changed, and the component is thus re-rendered.
This is because onDarkModeChange
is defined inline of the component and gets recreated every time the component re-renders. So even if you use React.memo
on the child components, they get re-rendered because the passed onDarkModeChange
function prop points to another reference every time.
This is why it is crucial to understand the identity of values. In contrast to recreated primitive values like numbers, a recreated function points to another “cell” in memory. That’s why the function values differ.
We can fix this with the useCallback
Hook. In addition, we do not necessarily need to use React.memo
because it’s not really a problem to get the child components re-rendered in our example. However, we want to execute the effect only when the interval
value or the darkMode
value changes:
import React, { useState, useEffect, useCallback } from "react"; const Counter = ({ interval, onDarkModeChange }) => { // ... }; const IntervalConfig = ({ onDarkModeChange }) => { // ... }; const EffectsDemoProps = () => { // .. const onDarkModeChange = useCallback(() => { return darkMode ? "🌙" : "🌞"; }, [darkMode]); // ... };
With useCallback
, React only creates a new function whenever one of the dependencies changes — in our case, the darkMode
state variable. With this in place, our example works as expected:
useCallback
with useContext
Suppose we modify the example and use React Context with the useContext
Hook instead of passing down props to the child components. In that case, we still need to use useCallback
for the onDarkModeChange
dependency. The reasons are the same as in the previous section:
import React, { useState, useEffect, useCallback, useContext } from "react"; const EffectsContext = React.createContext(null); const Counter = ({ interval }) => { const [count, setCount] = useState(0); const { onDarkModeChange } = useContext(EffectsContext); useEffect(() => { const counterInterval = setInterval(function () { setCount((prev) => prev + 1); }, interval); return () => clearInterval(counterInterval); }, [interval, onDarkModeChange]); return <p>and the counter counts {count}</p>; }; const IntervalConfig = () => { const [interval, setInterval] = useState(1000); const onChange = (evt) => setInterval(evt.target.value); return ( <div> <input type="text" value={interval} onChange={onChange} /> <Counter interval={interval} /> </div> ); }; const EffectsDemoContext = () => { const [numberClicks, setNumberClicks] = useState(0); const [darkMode, setDarkMode] = useState(false); const onDarkModeChange = useCallback(() => { return darkMode ? "🌙" : "🌞"; }, [darkMode]); return ( <div style={ darkMode ? { backgroundColor: "black", color: "white" } : { backgroundColor: "white", color: "black" } } > <label htmlFor="darkMode">dark mode</label> <input name="darkMode" type="checkbox" checked={darkMode} onChange={() => setDarkMode((prev) => !prev)} /> <p> <button onClick={() => setNumberClicks((prev) => prev + 1)}> click </button> <span> Number clicks: {numberClicks}</span> </p> <EffectsContext.Provider value={{ onDarkModeChange }}> <IntervalConfig /> </EffectsContext.Provider> </div> ); };
useEffect
inside of custom Hooks
Custom Hooks are awesome because they lead to various benefits:
- Reusable code
- Smaller components because of outsourced code (effects)
- More semantic code due to the function calls of the custom Hooks inside of components
- Effects can be tested when used inside of custom Hooks, as we’ll see in the next section
The following example represents a custom Hook for fetching data. We moved the useEffect
code block into a function representing the custom Hook. Note that this is a rather simplified implementation that might not cover all your project’s requirements. You can find more production-ready custom fetch Hooks here:
const useFetch = (url, initialValue) => { const [data, setData] = useState(initialValue); const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async function () { try { setLoading(true); const response = await axios.get(url); if (response.status === 200) { setData(response.data); } } catch (error) { throw error; } finally { setLoading(false); } }; fetchData(); }, [url]); return { loading, data }; }; function EffectsDemoCustomHook() { const { loading, data } = useFetch( "https://jsonplaceholder.typicode.com/posts/" ); return ( <div className="App"> {loading && <div className="loader" />} {data?.length > 0 && data.map((blog) => <p key={blog.id}>{blog.title}</p>)} </div> ); }
The first statement within our React component, EffectsDemoCustomHook
, uses the custom Hook called useFetch
. As you can see, using a custom Hook like this is more semantic than using an effect directly inside the component.
Business logic is nicely abstracted out of the component. We have to use our custom Hook’s nice API that returns the state variables loading
and data
.
The effect inside of the custom Hook is dependent on the scope variable url
that is passed to the Hook as a prop. This is because we have to include it in the dependency array. So even though we don’t foresee the URL changing in this example, it’s still good practice to define it as a dependency. As mentioned above, there is a chance that the value will change at runtime in the future.
Additional thoughts on functions used inside of effects
If you take a closer look at the last example, we defined the function fetchData
inside the effect because we only use it there. This is a best practice for such a use case. If we define it outside the effect, we need to develop unnecessarily complex code:
const useFetch = (url, initialValue) => { const [data, setData] = useState(initialValue); const [loading, setLoading] = useState(true); const fetchData = useCallback(async () => { try { setLoading(true); const response = await axios.get(url); if (response.status === 200) { setData(response.data); } } catch (error) { throw error; } finally { setLoading(false); } }, [url]); useEffect(() => { fetchData(); }, [fetchData]); return { loading, data }; };
As you can see, we need to add fetchData
to the dependency array of our effect. In addition, we need to wrap the actual function body of fetchData
with useCallback
with its own dependency (url
) because the function gets recreated on every render.
By the way, if you move function definitions into effects, you produce more readable code because it is directly apparent which scope values the effect uses. The code is even more robust.
Furthermore, if you do not pass dependencies into the component as props or context, the ESLint plugin “sees” all relevant dependencies and can suggest forgotten values to be declared.
Using async functions inside of useEffect
If you recall our useEffect
block inside of the useFetch
custom Hook, you might ask why we need this extra fetchData
function definition. Can’t we refactor our code like so?
useEffect(async () => { try { setLoading(true); const response = await axios.get(url); if (response.status === 200) { setData(response.data); } } catch (error) { throw error; } finally { setLoading(false); } }, [url]);
I’m glad you asked, but no! The following error occurs:
The mighty ESLint plugin also warns you about it.
The reason is that this code returns a promise, but an effect can only return void or a cleanup function.
Unit testing of effects
Extracting useEffect
blocks into custom Hooks allows for unit testing them because you don’t have to deal with the actual React component. This is a significant benefit.
Some time ago, I wrote an article about unit testing custom Hooks with react-hooks-testing-library. This is one possibility to test the effects.
The following snippet is a Jest example that tests data fetching even with changing one of the effect’s dependencies (url
) during runtime:
import { renderHook } from "@testing-library/react-hooks"; import axios from "axios"; import MockAdapter from "axios-mock-adapter"; // import custom hook - in this example extracted to a separate file import useFetch from "./useFetch"; test("useFetch performs multiple GET requests for different URLs", async () => { // fetch 1 const initialValue = "initial value"; const mock = new MockAdapter(axios); const mockData = 1; const url = "http://mock"; mock.onGet(url).reply(200, mockData); const { result, waitForNextUpdate } = renderHook(() => useFetch(url, initialValue) ); expect(result.current.data).toEqual("initial value"); expect(result.current.loading).toBeTruthy(); await waitForNextUpdate(); expect(result.current.data).toEqual(1); expect(result.current.loading).toBeFalsy(); // fetch 2 const url2 = "http://mock2"; const mockData2 = 2; mock.onGet(url2).reply(200, mockData2); const initialValue2 = "initial value 2"; const { result: result2, waitForNextUpdate: waitForNextUpdate2 } = renderHook( () => useFetch(url2, initialValue2) ); expect(result2.current.data).toEqual("initial value 2"); expect(result2.current.loading).toBeTruthy(); await waitForNextUpdate2(); expect(result2.current.data).toEqual(2); expect(result2.current.loading).toBeFalsy(); });
useFetch
is wrapped in a renderHook
function call. This provides the correct context to execute the custom Hook without violating the rules of Hooks.
To perform the actual network call, we utilize waitForNextUpdate
. This allows us to wait for the asynchronous function to return to check the response from the network call. With this set, we can assert the result of our Hook. In our test, we mocked the actual network call with axios-mock-adapter.
You can also find this code in a CodeSandbox.
Some more useEffect
receipts
In this section, I’ll show you some handy patterns that might be useful.
Execute an effect only once when a certain condition is met
As we already know, you control the execution of effects mainly with the dependency array. Every time one of the dependencies has changed, the effect is executed. Mostly, you should design your components to execute effects whenever a state changes, not just once.
Sometimes, however, you want to do precisely this — e.g., when a certain event has occurred. You can do this with flags that you use within an if
statement inside of your effect. The useRef
Hook is a good choice if you don’t want to add an extra render (which would be problematic most of the time) when updating the flag. In addition, you do not have to add the ref to the dependency array.
The following example calls the function trackInfo
from our effect only if the following conditions are met:
- The user clicked the button at least once
- The user has ticked the checkbox to allow tracking
After the checkbox is ticked, the tracking function should only be executed after the user clicks once again on the button:
function EffectsDemoEffectConditional() { const [count, setCount] = useState(0); const [trackChecked, setTrackChecked] = useState(false); const shouldTrackRef = useRef(false); const infoTrackedRef = useRef(false); const trackInfo = (info) => console.log(info); useEffect(() => { console.log("useEffect"); if (shouldTrackRef.current && !infoTrackedRef.current) { trackInfo("user found the button component"); infoTrackedRef.current = true; } }, [count]); console.log("render"); const handleClick = () => setCount((prev) => prev + 1); const handleCheckboxChange = () => { setTrackChecked((prev) => { shouldTrackRef.current = !prev; return !prev; }); }; return ( <div> <p> <label htmlFor="tracking">Declaration of consent for tracking</label> <input name="tracking" type="checkbox" checked={trackChecked} onChange={handleCheckboxChange} /> </p> <p> <button onClick={handleClick}>click me</button> </p> <p>User clicked {count} times</p> </div> ); }
In this implementation, we utilized two refs: shouldTrackRef
and infoTrackedRef
. The latter is the “gate” to guarantee that the tracking function is only invoked once after the other conditions are met.
The effect is rerun every time count
changes, i.e., whenever the user clicks on the button. Our if
statement checks the conditions and executes the actual business logic only if it evaluates to true
:
The log message user found the button component
is only printed once after the right conditions are met.
Access data from previous renders
If you need to access some data from the previous render cycle, you can leverage a combination of useEffect
and useRef
:
function EffectsDemoEffectPrevData() { const [count, setCount] = useState(0); const prevCountRef = useRef(); useEffect(() => { console.log("useEffect", `state ${count}`, `ref ${prevCountRef.current}`); prevCountRef.current = count; }, [count]); const handleClick = () => setCount((prev) => prev + 1); console.log("render"); return ( <div> <p> <button onClick={handleClick}>click me</button> </p> <p> User clicked {count} times; previous value was {prevCountRef.current} </p> </div> ); }
We synchronize our effect with the state variable count
so that it is executed after the user clicks on the button. Inside of our effect, we assign the current value of the state variable to the mutable current
property of prevCountRef
. We output both values in the JSX section:
On loading this demo, on initial render, the state variable has the initial value of the useState
call. The ref value is undefined
. It demonstrates once more that effects are run after render. When the user clicks, it works as expected.
When not to use useEffect
There are some situations in which you should avoid using useEffect
due to potential performance concerns.
1. Transforming data for rendering
If you need to transform data before rendering, then you don’t need useEffect
. Suppose you are showing a user list and only want to filter the user list based on some criteria. Maybe you only want to show the list of active users:
export const UserList = ({users}: IUserProps) => { // the following part is completely unnecessary. const [filteredUsers , setFilteredUsers] = useState([]) useEffect(() => { const activeUsers = users.filter(user => user.active) setFilteredUsers(activeUsers) ,[users]) return <div> {filteredUsers.map(user => <div> {user.name} </div>)} </div> }
Here you can just do the filtering and show the users directly, like so:
export const UserList = ({users}: IUserProps) => { const filteredUsers = users.filter(user => user.active) return <div> {filteredUsers.map(user => <div> {user.name} </div>)} </div> }
This will save you time and improve the performance of your application.
2. Handling user events
You don’t need useEffect
for handling user events. Let’s say you want to make a POST request once a user clicks on a form submit button. The following piece of code is inspired from React’s documentation:
function Form() { // Avoid: Event-specific logic inside an Effect const [jsonToSubmit, setJsonToSubmit] = useState(null); useEffect(() => { if (jsonToSubmit !== null) { post('/api/register', jsonToSubmit); } }, [jsonToSubmit]); function handleSubmit(e) { e.preventDefault(); setJsonToSubmit({ firstName, lastName }); } }
In the above code, you can just make the post request once the button is clicked. But you are cascading the effect, so once the useEffect
is triggered, it doesn’t have the complete context of what happened. This might cause issues in the future; instead, you can just make the POST request on the handleSubmit
function:
function Form() { function handleSubmit(e) { e.preventDefault(); const jsonToSubmit = { firstName, lastName }; post('/api/register', jsonToSubmit); } }
This is much cleaner and can help reduce future bugs.
Conclusion
Understanding the underlying design concepts and best practices of the useEffect
Hook is a key skill to master if you wish to become a next-level React developer.
If you started your React journey before early 2019, you have to unlearn your instinct to think in lifecycle methods instead of thinking in effects.
Adopting the mental model of effects will familiarize you with the component lifecycle, data flow, other Hooks (useState
, useRef
, useContext
, useCallback
, etc.), and even other optimizations like React.memo
.
LogRocket: Full visibility into your production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?
Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.
No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
Do you post on yb or twitter, so i can follow?
Thanks.
Great article! Thank you! A small feedback in “The cleanup function is called multiple times.”, I think you put in the wrong video 😉
Updated. Thanks for pointing that out.
Great write up! One question I have is what are the benefits to using useEffect with the ‘gate’ ref and if checks for api calls that need to run only when a certain event happens like a button click? To me it seems harder to read and adding more complexity than just calling the api from the button click handler. I know that its ‘the react way’ but why is it better?
hi Julio, yes I’m on Twitter: https://twitter.com/doppelmutzi
Thanks Tdot. Regarding your question, using a “gate / boolean flag pattern” should only rarely be necessary. Most of the time, it points to problematic design. I also had to think long and hard to find an example for this article. In my everyday work, I almost never had to do something like this.
This being said, in your described example you don’t need such a ref in combination with a button click. If I understand you right, you want to execute api calls whenever the user clicks a button – this falls into the normal pattern as I mentioned in the article “you should design your components to execute effects whenever a state changes, not just once”. If the user clicks your button, you update the state, a render happens, and then you can execute one or more effects depending on the changed state.
If I have misunderstood you, would you have a concrete example?
Regarding your statement that using this “gate pattern with refs” is more complicated – I am in complete agreement. Even with the small tracking example in this article, it is relatively complicated to execute a function only once when an event has occurred. You should avoid such an approach if possible (try to redesign your component) to have readable and understandable code.
} catch (error) {
throw error;
}
What is that??
This code is part of a simplified custom fetch hook and just re-throws the error again. In a real world project, you would most likey have a more advanced error handling, e.g., have another error state and return it to the callee to present some kind of error message / view.
Clearest and most comprehensive article on useEffect to date. Thank you!
Hello, I have a question about useEffect with functions in contexts. In your example you are using useCallback to avoid recreating the function but are creating a new object in the value of the provider. I have this confusion because of this https://reactjs.org/docs/context.html#caveats. So is it ok to do it like in your example or will it cause unintentional re-renders like in the example of the react docs? Thanks
Hello Alejandro, that’s a really good question! Yes, you’re right, there is a new object created with the following inline code value={{ onDarkModeChange }} which might lead to more re-renders of the IntervalComponent as necessary. So as you suggested with the react docu link, we could try to extract this object (maybe with useMemo?). I need to check this. However, my goal during the time of writing the article to make sure that the useEffect in the Counter component will not be invoked because of the re-creation of the onDarkModeChange function. Because we used useCallback in the EffectsDemoContext component and we do only use the same function reference all the time because of destructuring, the useEffect dependency is “stable”:
const { onDarkModeChange } = useContext(EffectsContext);
I hope this was helpful for you!
it is a great article ,
Complete justice to the title!
This is a very good, and necessary article on useEffect, thank you for writing it.
I have to say, though, that the direction React is going scares me to death. One of the best things about React when I started using it 5 years ago is that it was easy to read and understand what was going on. This is patently false now. Take an experienced Javascript developer who has been using any other client-side tool for 5+ years, even non-hooks React, and show them the examples in this article. They will have absolutely no idea what is going on. How could they possibly understand what a function (useEffect) that takes a function and returns a function, with an optional data array does? The very fact that eslint has to have a god-level plugin to handle a dependency array should tell the developers that they have gone way, way off track.
The same example using objects might be complicated as well, but with well-named functions like componentDidMount it can be figured out without a deep dive into the docs and an article like this one. (This is a big deal when hiring new developers that have to go in and make minor changes to existing code.) Now take a code base of a couple hundred thousand lines, and you can see how much of a problem this becomes.
I understand the argument for hooks. I understand that it is better for solving some specific problems, and is great for small, uncomplicated projects. It is a nice *optional* addition. Yet having written some very complicated front ends, I can’t imagine creating a new project with React Hooks. I just hope the React devs never get rid of the object-based interface, or a mountain of rewriting is going to cripple a lot of companies for years.
Again, thanks for writing this, as we have no choice but to follow the React team’s lead, and the React docs are fairly trivial with their examples. This is much, much better. I have recently started writing all new code in Hooks, and am starting to hit all of the issues in this article (even for a relatively simple component).
Thank you very much John. I really appreciate your kind words.
In addition, I have the same thoughts like you. I have very good devs in my team but they do struggle sometimes with hooks and sometimes don’t even know because they don’t know some related concepts. You have to investigate and practice heavily to master hooks/React.
We had for many weeks wrong usage of hooks because we had a wrong configuration regarding the eslint hook plugin. The consequences were we built the app around wrong/missing dependencies. After turning on the eslint plugin it was not easy to add the right deps and fix the app again.
As you said the class based approach was more explicit and many devs had less problems.
I hope React gets easier again. There are some new frameworks that seems to be easier.
Hi! I forgot to mention here my thanks! This is a really great article, I follow up everything here with exercises and it really helps me a lot to understand and every day as a good practice for my React Project.
I congratulate you for this great job! Thanks again!
Hey Patricio, that’s awesome. I’m glad the article helped you!
Do you have any guidelines for dependencies that use array values? they seem to suffer to similar side effects as functions do, since [1,2,3] === [1,2,3] is false.
Hi Shai, yes you’re right. The first thing I would do is to check if I can restructure my design to get rid of the array in the first place. If this is not possible, you most likely need useMemo. I made a quick research and found a “workaround” with JSON.stringify. Let’s take a look here, maybe this helps:
https://stackoverflow.com/a/59468261
https://github.com/facebook/react/issues/14476#issuecomment-471199055
Hi there is a mistake in the recording showing that exclduing count as dependency from useEffect will avoid cleanUp function from being called on every render. Your recording shows that ‘useEffect’ will be printed upon each execution of callback of setInterval, where as in reality it won’t. This is because you have excluded count variable from dependencies. So unless you have moved the console.log(“useEffect”) to your callback function passed to setInterval, the “useEffect” will be only printed once.