- Understand side effects in programming
- Use the
useEffect
hook to write side effects in components - Control when the side effects run by using a dependencies array with
useEffect
In this lesson, we'll talk about how to use side effects in our function
components with the useEffect
hook, and how to run additional code in our
components that isn't triggered by a user event such as clicking a button.
Here's a quick recap of some of the key concepts we've learned about React components:
- A component is a function that takes in props and returns JSX
- When we call
ReactDOM.render
and pass in our components, it will render all of our components by calling our component functions, passing down props, and building the DOM elements out of our components' JSX - When a React app's state is updated by calling the
setState
function, React will re-render the component, along with all of its children
When we think about the functionality of a function, we generally think about its return value. However, functions can also have side effects:
an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the main effect) to the invoker of the operation. — Wikipedia on Side Effects
Put more simply, if we call a function and that function causes change in our application outside of the function itself, it's considered to have caused a side effect. Things like making network requests, accessing data from a database, writing to the file system, etc. are common examples of side effects in programming.
In terms of a React component, the main effect of the component is to return some JSX. That's been true with all of the components we've been working with! One of the first rules we learned about function components is that they take in props, and return JSX.
However, it's often necessary for a component to perform some side effects in addition to its main job of returning JSX. For example, we might want to:
- Fetch some data from an API when a component loads
- Start or stop a timer
- Manually change the DOM
- Subscribe to a chat service
In order to handle these kinds of side effects within our components, we'll need
to use another special hook from React: useEffect
.
To use the useEffect
hook, we must first import it:
import React, { useEffect } from "react";
Then, inside our component, we call useEffect
and pass in a callback
function to run as a side effect:
function App() {
useEffect(
// side effect function
() => {
console.log("useEffect called");
}
);
console.log("Component rendering");
return (
<div>
<button>Click Me</button>
<input type="text" placeholder="Type away..." />
</div>;
)
}
If you run the example code now, you'll see the console messages appear in this order:
- Component rendering
- useEffect called
So we are now able to run some extra code as a side effect any time our component is rendered.
By using this Hook, you tell React that your component needs to do something after render. React will remember the function you passed (we'll refer to it as our "effect"), and call it later after performing the DOM updates. — React docs on the useEffect hook
Let's add some state into the equation, and see how re-rendering the component
by updating state interacts with our useEffect
hook:
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
useEffect(() => {
console.log("useEffect called");
});
console.log("Component rendering");
return (
<div>
<button onClick={() => setCount((count) => count + 1)}>
I've been clicked {count} times
</button>
<input
type="text"
placeholder="Type away..."
value={text}
onChange={(e) => setText(e.target.value)}
/>
</div>
);
}
Try clicking the button or typing in the input field to trigger updates in state. Every time state is set, we should also see those same two console messages in the same order:
- Component rendering
- useEffect called
By default, useEffect
will run our side effect function every time the
component re-renders.
render -> useEffect -> setState -> re-render -> useEffect
Sometimes we only want to run our side effect in certain conditions. For
example: imagine we're using the useEffect
hook to fetch some data from an
external API (a common use case for useEffect
). We don't want to make a
network request every time our component is updated, only the first time our
component renders.
If we write a component that updates state from inside the useEffect
callback,
we'll see an issue:
function DogPics() {
const [images, setImages] = useState([]);
useEffect(() => {
fetch("https://dog.ceo/api/breeds/image/random/3")
.then((r) => r.json())
.then((data) => {
// setting state in the useEffect callback
setImages(data.message);
});
});
return (
<div>
{images.map((image) => (
<img src={image} key={image} />
))}
</div>
);
}
Running this code will result in an endless loop of fetch
requests (until the
API kicks us out for hitting the rate limit 👀). We'd end up in a cycle like
this:
render -> useEffect -> setImages -> render -> useEffect -> setImages -> render -> etc...
So how can we control when useEffect
will run our side effect function?
React gives us a way to control when the side effect will run by passing a
second argument to useEffect
, a dependencies array. Let's set that up in
our App
component:
useEffect(
// 1st arg: side effect (callback function)
() => console.log("useEffect called"),
// 2nd arg: dependencies array
[count]
);
Now, if you try running the code again, the side effect will only run when the
count
variable changes. We won't see any console messages from useEffect
when typing in the input — we'll only see them when clicking the button!
We can also pass in an empty array of dependencies as a second argument, like this:
useEffect(() => {
console.log("useEffect called");
}, []); // second argument is an empty array
Now, the side effect will only run the first time our component renders! That same approach can be used to fix the infinite loop we saw in the fetch example as well:
useEffect(() => {
fetch("https://dog.ceo/api/breeds/image/random/3")
.then((r) => r.json())
.then((data) => {
setImages(data.message);
});
}, []);
In this example, our component rendering cycle now looks like this:
render -> useEffect -> setImages -> render
Running a fetch
request as a side effect is one great example of when you'd
use the useEffect
hook and we'll explore that in more detail in the coming
lessons. For now, let's take a look at a couple of other examples where you
might use the useEffect
hook.
One kind of side effect we can demonstrate here is updating parts of the
webpage outside of the React DOM tree. React is responsible for all the
DOM elements rendered by our components, but there are some parts of the webpage
that live outside of this tree. Take, for instance, the <title>
of our page
— this is what shows up in the browser tab, like this:
Updating this part of the page would be considered a side effect, so let's use
useEffect
to update it!
useEffect(() => {
document.title = text;
}, [text]);
Here, what we're telling React is:
"Hey React! When my component renders, I also want you to update the
document's title. But you should only do that when the text
variable changes."
Let's add another side effect, this time running a setTimeout
function. We
want this function to reset the count
variable back to 0 after 5 seconds.
Running a setTimeout
is another example of a side effect, so once again, let's
use useEffect
:
useEffect(() => {
setTimeout(() => setCount(0), 5000);
}, []);
With this side effect, we're telling React:
"Hey React! When my App component renders, I also want you to set this timeout function. In 5 seconds, you should update state and set the count back to 0. You should only set this timeout function once, I don't want a bunch of timeouts running every time my component updates. kthxbye!"
All together, here's what our updated component looks like:
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
useEffect(() => {
document.title = text;
}, [text]);
useEffect(() => {
setTimeout(() => setCount(0), 5000);
}, []);
console.log("Component rendering");
return (
<div>
<button onClick={() => setCount((count) => count + 1)}>
I've been clicked {count} times
</button>
<input
type="text"
placeholder="Type away..."
value={text}
onChange={(e) => setText(e.target.value)}
/>
</div>
);
}
Explore this code to familiarize yourself with useEffect
, and see what changes
by changing the dependencies array. It's also a good idea to add some console
messages or put in a debugger to see exactly when the side effects will run.
Here's a quick guide on how to use the second argument of useEffect
to control
when your side effect code will run:
useEffect(() => {})
: No dependencies array- Run the side effect every time our component renders (whenever state or props change)
useEffect(() => {}, [])
: Empty dependencies array- Run the side effect only the first time our component renders
useEffect(() => {}, [variable1, variable2])
: Dependencies array with elements in it- Run the side effect any time the variable(s) change
Or, to put it another way:
So far, we've been working with components solely for rendering to the DOM based on JSX, and updating based on changes to state. It's also useful to introduce side effects to our components so that we can interact with the world outside of the React DOM tree and do things like making network requests or setting timers.