DEV Community

Cover image for Awesome React-Hooks - Part 2 - useEffect
Timothée Clain
Timothée Clain

Posted on

Awesome React-Hooks - Part 2 - useEffect

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.

Top comments (0)