React useEffect hook with code examples
So you learned how to use React useState and how to emulate the traditional setState in a functional React component.
But now you might be scratching your head and asking yourself, “How do I emulate a React component lifecycle?”
Don’t worry, in this article will go over:
- What is React
useEffect
- How to use React useEffect
- How to replicate the
componentDidMount
lifecycle - How to replicate the
componentWillUnmount
lifecycle - How to create self-sufficient code for
componentDidUpdate
lifecycle
So if you’re not familiar with how to use it, read a previous article that covers about it; Introduction to React hooks.
Come back when you’re ready!
What is React useEffect?
React useEffect
is a function that gets executed for 3 different React component lifecycles.
Those lifecycles are componentDidMount
, componentDidUpdate
, and componentWillUnmount
lifecycles.
Basic usage of useEffect
import React, { useState } from 'react';
const App = () => {
const [message, setMessage] = useState('Hi there, how are you?');
return <h1>{message}</h1>
};
export default App;
In the code example above, I’m using React useState
to save a message.
I’m then grabbing my state variable, message
, and I’m printing it to the screen.
But now I want to useEffect
to change the message a second after the component has mounted.
import React, { useState, useEffect } from 'react';
const App = () => {
const [message, setMessage] = useState('Hi there, how are you?');
useEffect(() => {
console.log('trigger use effect hook');
setTimeout(() => {
setMessage("I'm fine, thanks for asking.");
}, 1000)
})
return <h1>{message}</h1>
};
export default App;
I’ve imported useEffect
from the React library and I’ve added my effect under the useState
line.
useEffect
accepts a function as it’s first argument. This function handler will take care of any side effects you like when it gets run.
The function is a callback function after one of the React component lifecycle has been triggered.
It worked! But there’s a problem. Take a look at the console log
. The effect got triggered twice.
This behavior is not optimal, because if you have multiple effects or have new prop values being tossed from a parent component, it may trigger the effect multiple times.
This may cause inconsistency, weird side-effects, or freezing your app from an infinite loop.
Let’s optimize this code a little bit more.
React useEffect: The componentDidMount hook
The goal now is to execute the setMessage
function only on the componentDidMount
lifecycle.
That way if the App component receives any new props or if the state changes, the effect won’t be triggered again.
import React, { useState, useEffect } from 'react';
const App = () => {
const [message, setMessage] = useState('Hi there, how are you?');
useEffect(() => {
console.log('trigger use effect hook');
setTimeout(() => {
setMessage("I'm fine, thanks for asking.");
}, 1000)
}, []);
return <h1>{message}</h1>
};
export default App;
Can you see the difference from the old example?
If you haven’t caught it yet, I added an empty array bracket ([]
) as a second argument to the useEffect
hook function.
If you take a look at the console log it only shows “trigger use effect hook” once.
Here’s an example output with another console log message that says, “App component rendered,” after the effect function.
The second console message should only execute when the render lifecycle gets triggered.
If we take a look at the console log again, we can see the order of the lifecycles that it went through.
- Rendered lifecycle
componentDidMount
lifecycle- Rendered lifecycle
This is the normal behavior that you would see in a traditional React class component.
By running an empty array []
as a second argument, you’re letting React know that your useEffect
function doesn’t depend on any values from props or state.
This will help you avoid the componentDidUpdate
lifecycle.
React useEffect: The componentWillUnmount hook
I showed an example how to avoid a trigger from a componentDidUpdate
lifecycle with useEffect
.
But what if you have code that needs to get cleared up on a componentWillUnmount
cycle?
How do we replicate a componentWillUnmount
lifecycle?
P.S. this lifecycle is also known as, the cleanup in a React hook function.
In the next example I will demonstrate a use case where you’ll need to clean up your code when a component will unmount.
const WindowWidthSize = () => {
const [windowWidthSize, setWindowWidthSize] = React.useState(0);
React.useEffect(() => {
function handleResize(e) {
const { width } = document.body.getBoundingClientRect();
setWindowWidthSize(Math.ceil(width));
}
window.addEventListener('resize', handleResize)
}, []);
return (
<h1>
The window size {windowWidthSize} pixels
</h1>
)
};
In the image above, I have created a new function component called, WindowWidthSize
.
The objective of this component is to print out the width size of the current window.
As you see can see, I’m using useState
to keep track of the width size.
And right below it I’m adding a window event listener on the resize event.
So every time the user resizes the browser, it will get the new width, save it into state, and print out the new width size.
Okay, so here we have another React component that uses the component WindowWidthSize
, and it has a magical button.
When a user clicks this magical button, the WindowWidthSize
component will vanish before your eyes.
Great, the code works. And if you see on the bottom right there is a blue highlight section called Window.
The blue highlight shows the window event listener I have added.
When I click on the magical button, the WindowWidthSize
component will no longer exist. But there’s a small problem.
The window event listener is still lingering around.
Situations like these are bad because this is what causes memory leaks in your app.
And you don’t want to be greedy with your customers limited bandwidth.
Let’s use the clean up technique that useEffect
gives us, to get rid of this lingering window event.
const WindowWidthSize = () => {
const [windowWidthSize, setWindowWidthSize] = React.useState(0);
React.useEffect(() => {
function handleResize(e) {
const { width } = document.body.getBoundingClientRect();
setWindowWidthSize(Math.ceil(width));
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<h1>
The window size {windowWidthSize} pixels
</h1>
)
};
I added a return function inside the useEffect
function.
And inside the return function I’m removing the even listener that I originally added.
When a functional component un-mounts the logic inside the return function will get executed.
So remember to clean up your code if necessary by returning a function inside the useEffect
function.
React useEffect: The componentWillUpdate hook
By default useEffect
will trigger anytime an update happens to the React component.
This means if the component receives new props from its parent component or even when you change the state locally, the effect will run again.
In case you need the effect to trigger on a componentDidUpdate
lifecycle, you want to try and make it as self-sufficient as possible.
If you don’t, you will run into an infinite loop of updates, and just run your computer hot.
const Counter = () => {
const [counter, setCounter] = React.useState(0);
React.useEffect(() => {
setCounter(c => c + 1);
}, []);
return (
<div style={{textAlign: 'center'}}>
<h1>Counter: {counter}</h1>
</div>
);
};
Right above is a simple counter app. All it does is print a number to the user.
P.S. useState also accepts functions as an argument. This might be a better choice if you want a more accurate representation of the previous data.
I want this counter to increase on every second.
const Counter = () => {
const [counter, setCounter] = React.useState(0);
React.useEffect(() => {
const s = setInterval(() => {
setCounter(c => c + 1);
}, 1000);
}, []);
return (
<div style={{textAlign: 'center'}}>
<h1>Counter: {counter}</h1>
</div>
);
};
This is good, but not good enough!
If you have multiple setStates
inside this component or even receiving new props, this can throw the useEffect
hook off track.
Causing your app to not be synchronized or just freeze.
const Counter = () => {
const [counter, setCounter] = React.useState(0);
React.useEffect(() => {
const s = setInterval(() => {
setCounter(c => c + 1);
}, 1000);
return () => clearInterval(s);
}, [counter]);
return (
<div style={{textAlign: 'center'}}>
<h1>Counter: {counter}</h1>
</div>
);
};
Now it’s more self-sufficient.
One, I’ve added a clean up function to clear the interval whenever the component will unmount.
Two, I’ve added the counter variable inside the array bracket that’s found in the second argument of the useEffect
function.
This tells React to only trigger the effect when counter is a different value.
If counter has not changed in value, the effect won’t execute.
This is helpful because you can safely add multiple useEffects
, setStates
, or even pass down new prop values, and it won’t desynchronize your counter component.
Using Async and Await in React useEffect
React.useEffect
does NOT allow you to add async
in the callback function.
// This will cause your React application to break!
React.useEffect(async() => {
// ... stuff
}, []);
React is expecting a regular function and not a promise.
But if you’d like to use async/await
you can move your code into its own function, and invoke it inside React.useEffect()
.
async function fetchCats() {
try {
const response = await fetch('url/to/cat/api');
const { cats } = await response.json();
console.log(cats) // [ { name: 'Mr. Whiskers' }, ...cats ]
} catch(e) {
console.error(e);
}
}
React.useEffect(() => {
fetchCats();
}, []);
Conclusion
React.useEffect
is a basic hook that gets triggered on a combination of 3 React component lifecycles:
- componentDidMount
- componentDidUpdate
- componentWillUnmount
If you’re planning to use React hooks you must know how to execute your effect on the right time.
Otherwise you might run into some problems for your users.
I like to tweet about React and post helpful code snippets. Follow me there if you would like some too!