How the UseEffect() react hook work
The useEffect
hook gives you a better way to think about your component life cycle.
With useEffect
, you can handle lifecycle events directly inside function components. Namely, three of them: componentDidMount
, componentDidUpdate
, and componentWillUnmount
. All with one function! Crazy, I know. Let’s see an example.
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom'; function LifecycleDemo() { useEffect(() => { console.log('render!'); return () => console.log('unmounting...'); }) return "I'm a lifecycle demo";
} function App() { const [random, setRandom] = useState(Math.random()); const [mounted, setMounted] = useState(true); const reRender = () => setRandom(Math.random()); const toggle = () => setMounted(!mounted); return ( <> <button onClick={reRender}>Re-render</button> <button onClick={toggle}>Show/Hide LifecycleDemo</button> {mounted && <LifecycleDemo/>} </> );
} ReactDOM.render(<App/>, document.querySelector('#root'));
Click the Show/Hide button. Look at the console. It prints “unmounting” before it disappears, and “render!” when it reappears.
Now, try the Re-render button. With each click, it prints “render!” and it prints “umounting”. That seems weird…
Why is it “unmounting” with every render?
Well, the cleanup function you can (optionally) return from useEffect
isn’t only called when the component is unmounted. It’s called every time before that effect runs – to clean up from the last run. This is actually more powerful than the componentWillUnmount
lifecycle because it lets you run a side effect before and after every render, if you need to.
Not Quite Lifecycles
useEffect
runs after every render (by default), and can optionally clean up for itself before it runs again.
Rather than thinking of useEffect
as one function doing the job of 3 separate lifecycles, it might be more helpful to think of it simply as a way to run side effects after render – including the potential cleanup you’d want to do before each one, and before unmounting.
Prevent useEffect From Running Every Render
If you want your effects to run less often, you can provide a second argument – an array of values. Think of them as the dependencies for that effect. If one of the dependencies has changed since the last time, the effect will run again. (It will also still run after the initial render)
const [value, setValue] = useState('initial'); useEffect(() => { console.log(value);
}, [value])
Another way to think of this array: it should contain every variable that the effect function uses from the surrounding scope. So if it uses a prop? That goes in the array. If it uses a piece of state? That goes in the array.
Only Run on Mount and Unmount
You can pass the special value of empty array []
as a way of saying “only run on mount and unmount”. So if we changed our component above to call useEffect
like this:
useEffect(() => { console.log('mounted'); return () => console.log('unmounting...');
}, [])
Then it will print “mounted” after the initial render, remain silent throughout its life, and print “unmounting…” on its way out.
This comes with a big warning, though: passing the empty array is prone to bugs. It’s easy to forget to add an item to it if you add a dependency, and if you miss a dependency, then that value will be stale the next time useEffect
runs and it might cause some strange problems.
Focus On Mount
Sometimes you just want to do one tiny thing at mount time, and doing that one little thing requires rewriting a function as a class.
In this example, let’s look at how you can focus an input control upon first render, using useEffect
combined with the useRef
hook.
import React, { useEffect, useState, useRef } from "react";
import ReactDOM from "react-dom"; function App() { const inputRef = useRef(); const [value, setValue] = useState(""); useEffect( () => { console.log("render"); }, [inputRef] ); return ( <input ref={inputRef} value={value} onChange={e => setValue(e.target.value)} /> );
} ReactDOM.render(<App />, document.querySelector("#root"));
At the top, we’re creating an empty ref with useRef
. Passing it to the input’s ref
prop takes care of setting it up once the DOM is rendered. And, importantly, the value returned by useRef
will be stable between renders – it won’t change.
So, even though we’re passing [inputRef]
as the 2nd argument of useEffect
, it will effectively only run once, on initial mount. This is basically “componentDidMount” (except the timing of it, which we’ll talk about later).
Fetch Data With useEffect
Let’s look at another common use case: fetching data and displaying it. In a class component, you’d put this code in the componentDidMount
method. To do it with hooks, we’ll pull in useEffect
. We’ll also need useState
to store the data.
It’s worth mentioning that when the data-fetching portion of React’s new Suspense feature is ready, that’ll be the preferred way to fetch data. Fetching from useEffect
has one big gotcha (which we’ll go over) and the Suspense API is going to be much easier to use.
Here’s a component that fetches posts from Reddit and displays them:
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom"; function Reddit() { const [posts, setPosts] = useState([]); useEffect(() => { async function fetchData() { const res = await fetch( "https://www.reddit.com/r/reactjs.json" ); const json = await res.json(); setPosts(json.data.children.map(c => c.data)); } fetchData(); }); return ( <ul> {posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> );
} ReactDOM.render( <Reddit />, document.querySelector("#root")
);
You’ll notice that we aren’t passing the second argument to useEffect
here. This is bad. Don’t do this.
Passing no 2nd argument causes the useEffect
to run every render. Then, when it runs, it fetches the data and updates the state. Then, once the state is updated, the component re-renders, which triggers the useEffect
again. You can see the problem.
To fix this, we need to pass an array as the 2nd argument. What should be in the array?
Go ahead, think about it for a second.
…
…
The only variable that useEffect
depends on is setPosts
. Therefore we should pass the array [setPosts]
here. Because setPosts
is a setter returned by useState
, it won’t be recreated every render, and so the effect will only run once.
Fun fact: When you call useState
, the setter function it returns is only created once! It’ll be the exact same function instance every time the component renders, which is why it’s safe for an effect to depend on one. This fun fact is also true for the dispatch
function returned by useReducer
.
Re-fetch When Data Changes
Let’s expand on the example to cover another common problem: how to re-fetch data when something changes, like a user ID, or in this case, the name of the subreddit.
First we’ll change the Reddit
component to accept the subreddit as a prop, fetch the data based on that subreddit, and only re-run the effect when the prop changes:
function Reddit({ subreddit }) { const [posts, setPosts] = useState([]); useEffect(() => { async function fetchData() { const res = await fetch( `https://www.reddit.com/r/${subreddit}.json` ); const json = await res.json(); setPosts(json.data.children.map(c => c.data)); } fetchData(); }, [subreddit, setPosts]); return ( <ul> {posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> );
} ReactDOM.render( <Reddit subreddit='reactjs' />, document.querySelector("#root")
);
This is still hard-coded, but now we can customize it by wrapping the Reddit
component with one that lets us change the subreddit. Add this new App component, and render it at the bottom:
function App() { const [inputValue, setValue] = useState("reactjs"); const [subreddit, setSubreddit] = useState(inputValue); const handleSubmit = e => { e.preventDefault(); setSubreddit(inputValue); }; return ( <> <form onSubmit={handleSubmit}> <input value={inputValue} onChange={e => setValue(e.target.value)} /> </form> <Reddit subreddit={subreddit} /> </> );
} ReactDOM.render(<App />, document.querySelector("#root"));
The app is keeping 2 pieces of state here – the current input value, and the current subreddit. Submitting the input “commits” the subreddit, which will cause Reddit
to re-fetch the data from the new selection. Wrapping the input in a form allows the user to press Enter to submit.
btw: Type carefully. There’s no error handling. If you type a subreddit that doesn’t exist, the app will blow up. Implementing error handling would be a great exercise though! 😉
We could’ve used just 1 piece of state here – to store the input, and send the same value down to Reddit
– but then the Reddit
component would be fetching data with every keypress.
The useState
at the top might look a little odd, especially the second line:
const [inputValue, setValue] = useState("reactjs");
const [subreddit, setSubreddit] = useState(inputValue);
We’re passing an initial value of “reactjs” to the first piece of state, and that makes sense. That value will never change.
But what about that second line? What if the initial state changes? (and it will, when you type in the box)
Remember that useState
is stateful (read more about useState). It only uses the initial state once, the first time it renders. After that it’s ignored. So it’s safe to pass a transient value, like a prop that might change or some other variable.
A Hundred And One Uses
The useEffect
function is like the swiss army knife of hooks. It can be used for a ton of things, from setting up subscriptions to creating and cleaning up timers to changing the value of a ref.
One thing it’s not good for is making DOM changes that are visible to the user. The way the timing works, an effect function will only fire after the browser is done with layout and paint – too late, if you wanted to make a visual change.
For those cases, React provides the useMutationEffect
and useLayoutEffect
hooks, which work the same as useEffect
aside from when they are fired. Have a look at the docs for useEffect and particularly the section on the timing of effects if you have a need to make visible DOM changes.
This might seem like an extra complication. Another thing to worry about. It kinda is, unfortunately. The positive side effect of this (heh) is that since useEffect
runs after layout and paint, a slow effect won’t make the UI janky. The down side is that if you’re moving old code from lifecycles to hooks, you have to be a bit careful, since it means useEffect
is almost-but-not-quite equivalent to componentDidUpdate
in regards to timing.