Awesome React-Hooks - Part 2 - useEffect
Timothée Clain
Posted on November 1, 2018
This post is the second in a series about React hooks.
Part 1 :
In the last post, we saw how to use the useState
hook in React 16.7+. Today let's learn about useEffect
TLDR
useEffect
take a callback
function as arguments that will be re-run after each rerender of your functional component.
If this callback returns another function, this function will be called on the component unmount.
useEffect
, can take a second arguments: any[]
, that is a list of dependencies that should trigger a rewrite. These dependencies can be a prop, or another state produced by setState
.
Example with a persisted counter
Let's say we take our counter increment as before and we want the value to be persisted in localStorage,
we could use useEfffect
for this.
As a reminder, here is our base code:
import React, {useState, useEffect} from 'react'
function Counter() {
const [counter, setCounter] = useState(0)
// persist logic will be here
return {
counter,
setCounter
}
}
export const App = () => {
const {setCounter, counter} = Counter();
return <button onClick={() => setCounter(counter + 1)}>Change {counter}</button>
}
The Counter
function define a useState hook to store our counter value.
If we are defining :
useEffect(() => {
window.localStorage.setItem('counter', counter)
})
The setItem
operation will be run after each rerender.
We have one step left, to populate the counter value with the value from localStorage for the first time.
const [counter, setCounter] = useState(JSON.parse(window.localStorage.getItem('counter')) || 0)
The whole example can be found here:
https://stackblitz.com/edit/react-use-effect-hook?file=index.js
Cleanup function
If you return a function from the useEffect callback, this function will be cleaned. This is super useful if you need to unsubscribe from global events ... and so on.
Practical Example
Let say we got an async search box that displays the user list from GitHub.
We could use the combination of useState
and useEffect
to fetch dynamically the list from the query entered by the user.
As we did it before, let's create a custom hook function.
function useGithubUsers() {
const [query, setQuery] = useState("")
const [results, setResults] = useState([])
const [loading, setLoading] = useState(true)
// side effect here
// exposing only public api !
return {
query, setQuery, results, loading
}
}
So we are basically declaring three variables: query
(the current query search
), results
(an array of Github users), loading
(a loading indicator).
Here how we could use this custom hook:
export const App = () => {
const { setQuery, query, results, loading } = useGithubUsers();
return <div>
<input onChange={e => setQuery(e.target.value)} />
<ul>
{loading && <li>Loading</li>}
{
results && results.map((item, index) => <li key={index} >{item.login}</li>)
}
</ul>
</div>
}
What's cool with hooks, we can reason very easily about the lifecycle of our data.
Here, if we insert the useEffect Hook between useState
declarations and the return function, the side effect after all state
will be changed and the component rerender.
Let's fetch (pun intended) the GitHub users using the search
API from github.com.
if (query !== "") {
setLoading(true);
fetch(`https://api.github.com/search/users?q=${query}`, { method: "GET"}).then(req => {
return req.json();
}).then(data => {
setLoading(false)
setResults(data.items)
})
}
If you would run this code directly, you would have a big problem, cause the useEffect is rerun after each rerender (aka infinite loop in this case), so you'll need to use the second argument of the useEffect
function that takes an array of variables that need to change to run this effect (a la shouldComponentUpdate)
setEffect( () => {...}, [query])
If you provide an empty array, any code inside the effect would run only once when the component mounts, and any code inside the
dispose
returned function would run only on the unmount of the component.
By the way, you could use a prop here, giving you a neat way of reacting to props change!
The resulting code is :
import React, { useState, useEffect } from 'react'
import { render } from 'react-dom'
import Select from 'react-select';
import debounce from 'lodash.debounce';
function useGithubUsers() {
const [query, setQuery] = useState("")
const [results, setResults] = useState([])
const [loading, setLoading] = useState(false)
// each rerender
useEffect(debounce(() => {
if (query !== "") {
setLoading(true);
fetch(`https://api.github.com/search/users?q=${query}`, { method: "GET"}).then(req => {
return req.json();
}).then(data => {
setLoading(false)
setResults(data.items)
})
}
}, 300), [query])
return {
query,
setQuery,
setLoading,
results,
loading,
}
}
export const App = () => {
const { setQuery, query, results, loading } = useGithubUsers();
return <div>
<input onChange={e => setQuery(e.target.value)} />
<ul>
{loading && <li>Loading</li>}
{
results && results.map((item, index) => <li key={index} >{item.login}</li>)
}
</ul>
</div>
}
render(<App />, document.getElementById('root'));
You can test it live here:
https://stackblitz.com/edit/react-use-effect-hook-github?
Github has some rate limit on this API so if it does not work, wait a minute and retry.
Of course, this example is trivial and very basic.
Nothing prevents you to abstract a little more and reuse it for AJAX Requests :-)
Next
In the next post, we'll see how to optimize this logic by using the memoization hooks.
Posted on November 1, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.