Introduction
React’s useEffect
hook allows us to perform side effects in our component whenever it renders, such as fetching API data.
How it works
A useEffect
takes a callback, and this code is then ran depending on how you set up your useEffect
.
The useEffect
hook has a 2nd parameter which takes an array. This array is called the dependency array.
The dependency array will determine how often the component will render.
Below is a basic example where the useEffect
will only run once the component is mounted.
import React, { useEffect } from 'react';
function App() {
useEffect(() => {
// This is the callback that runs inside of the useEffect
console.log('useEffect has run');
}, []); // This is the dependency array
return <div>useEffect Example</div>;
}
export default App;
Dependency array options
We will now look at the different dependency array options.
The three options are:
- Run every time the component is rendered (no dependency array added).
- Run only the first time the component is rendered (empty array dependency array added).
- Run when a watched variable is updated (dependency array added with variable names added to it).
Let’s have a look at the three different ways you can set up your useEffect
.
1. Run every time component is rendered
If we don’t include a dependency array, then the useEffect
will run every time the component renders.
This option is not as used as often as the other options as we generally don’t want a useEffect
running every time the component renders, usually only when we need it to.
Below is a basic app with a counter
variable. There is a button to increment and a button to decrement the counter. There is a console.log('Component has rendered')
in the component itself that will log each time the component is rendered.
If you run this code, you will see that the useEffect
will run every time you click one of the buttons. You will see that both Component has rendered
and useEffect has run
will be logged to the console.
import React, { useEffect, useState } from 'react';
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
console.log('useEffect has run');
}); // No dependency array
function onIncrementCounter() {
setCounter(counter + 1);
}
function onDecrementCounter() {
setCounter(counter - 1);
}
console.log('Component has rendered');
return (
<div>
<div>Counter: {counter}</div>
<button onClick={onIncrementCounter}>+</button>
<button onClick={onDecrementCounter}>-</button>
</div>
);
}
export default App;
2. Run only when the component is mounted
If we include an empty dependency array, the useEffect
will only run the first time the component mounts. This is a more common option over having the useEffect
run each time the component re-renders.
We are using the same counter
example below.
When the component is first mounted, you’ll see the useEffect has run
being logged to the console. However, when you click the buttons, you will see that only Component has rendered
is logged to the console. This is because our useEffect
will only run when the component is mounted.
import React, { useEffect, useState } from 'react';
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
console.log('useEffect has run');
}, []); // Empty dependency array
function onIncrementCounter() {
setCounter(counter + 1);
}
function onDecrementCounter() {
setCounter(counter - 1);
}
console.log('Component has rendered');
return (
<div>
<div>Counter: {counter}</div>
<button onClick={onIncrementCounter}>+</button>
<button onClick={onDecrementCounter}>-</button>
</div>
);
}
export default App;
3. Watching a variable
The final option we have with useEffect
is to watch values and have the useEffect
run when these values are updated.
We have the same counter
example below; however, we’ve added the counter
value to the dependency array.
You’ll see that each time we click the buttons, we will get Component has rendered
logged out, and useEffect has run
.
NOTE: The
console.log
outputs in this example are the same as when we don’t include a dependency array. However, they are not operating the same.
import React, { useEffect, useState } from 'react';
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
console.log('useEffect has run');
}, [counter]); // Watching the `counter` variable
function onIncrementCounter() {
setCounter(counter + 1);
}
function onDecrementCounter() {
setCounter(counter - 1);
}
console.log('Component has rendered');
return (
<div>
<div>Counter: {counter}</div>
<button onClick={onIncrementCounter}>+</button>
<button onClick={onDecrementCounter}>-</button>
</div>
);
}
export default App;
useEffect
’s cleanup function
useEffect
comes with a cleanup function that allows you to cancel subscriptions, such as removing timers that are running.
The cleanup function is used as the return
in a useEffect
.
Consider the following example. It contains an <Example>
component that will start a timer. It also contains a button that will remove the <Example>
component.
import React, { useEffect, useState } from 'react';
function Example() {
// useEffect that starts a timer which runs every second
useEffect(() => {
setInterval(() => {
console.log('Timer running');
}, 1000);
}, []);
return <div>Example is showing</div>;
}
function App() {
const [showComponent, setShowComponent] = useState(true);
function onButtonClick() {
setShowComponent(false);
}
return (
<div>
{showComponent ? <Example /> : null}
<button onClick={onButtonClick}>Hide component</button>
</div>
);
}
export default App;
You’ll notice that if you press the button, the <Example>
component is no longer mounted; however, the console.log
s from the timer is still running.
This is problematic and can lead to many problems in your app. Instead, what should happen is that the timer should be cleared when the component unmounts so it stops running.
This is where our cleanup function from the useEffect
becomes useful. We can use the cleanup function to clear the timer.
In the example below, we’ve added a cleanup function that will clear the timer. When the component unmounts it, the timer will be stopped.
import React, { useEffect, useState } from 'react';
function Example() {
// useEffect that starts a timer which runs every second
useEffect(() => {
const timerId = setInterval(() => {
console.log('Timer running');
}, 1000);
// This is the cleanup function
return () => {
clearInterval(timerId);
};
}, []);
return <div>Example is showing</div>;
}
function App() {
const [showComponent, setShowComponent] = useState(true);
function onButtonClick() {
setShowComponent(false);
}
return (
<div>
{showComponent ? <Example /> : null}
<button onClick={onButtonClick}>Hide component</button>
</div>
);
}
export default App;
Lesson task
Goal
For the student to demonstrate they can use the useEffect
hook.
Brief
You will create useEffect
where an event listener is created while also performing a cleanup of the event listener.
NOTE: Lesson tasks do not get submitted on Moodle and are not assessed by tutors. They are mainly there for you to practise what you have learnt in the lesson.
Level 1 process
-
Start with a clean CRA, or reuse one you have created.
-
Add a
useEffect
hook to<App>
. -
Create an interval timer that logs “Timer running”.
-
Add a cleanup to the
useEffect
so that the timer is removed if the current component,<App>
, is removed.