DEV Community

Cover image for More on useEffect()
Ray Sy
Ray Sy

Posted on • Edited on

More on useEffect()

The useEffect hook can be confusing so hopefully this post will clear some things up. The purpose of the useEffect hook is to allow your code to react to state changes. To anyone familiar with stateful components, it replaces componentDidMount() and componentDidUpdate().
​​
TLDR
The useEffect() hook allows your program to react to changes in state. It replaces onComponentDidMount(), onComponentDidUpdate(), and onComponentWillUnmount(). Also, you can't directly pass an async function to useEffect(), because it is not allowed to return a promise.

Sandbox featuring a live search demo: https://codesandbox.io/s/live-search-demo-dvzbz?file=/src/App.js

Scenario

Let’s say we want to create live search/filter for our website - i.e. you type things into the search bar and the results below will automatically change without pressing a button. Initially our useEffect hook could look something like this:

const [query, setQuery] = useState('');
const [data, setData] = useState([]);
// other code ...
useEffect(() => {
    fetchData().then(data => {
        setData(data);
        console.log(data);
    }
});
Enter fullscreen mode Exit fullscreen mode

Note that I used plain promises instead of async-await more on that later.

This approach will "work", but there's a problem. If you check the console, it will be spammed with constant API calls. This is because by default, useEffect() will run every time the component re-renders aka every time state changes. As you can see that leads to an infinite loop where our useEffect() updates the state and the change of state triggers useEffect(). At best this leads to poor performance, and at worse could get your API key revoked.

The same problem existed with stateful components, and was often solved by checking to see which state changed, and then only updating when necessary.

componentDidUpdate(prevProps, prevState) {
    if (prevState.query !== this.state.query) {
        // do some update here
    }
    // otherwise do nothing
}
Enter fullscreen mode Exit fullscreen mode


With hooks we don't have access to prevProps but thats where the second argument for useEffect() becomes useful. Generally, there are three things we can pass.

  1. Nothing undefined. This means that useEffect() will run on every re-render of the component. Analogous to implementing componentDidMount() and componentDidUpdate with no checks .
  2. An empty array []. This means useEffect() will only run a single time. Analogous to implementing componentDidMount() .
  3. An array with value(s). React will check to see if the value(s) you passed in changed since the last render, and will fire useEffect() accordingly. Analogous to componentDidMount() and componentDidMount() with checks to see if the state property matches the prevState property. ​ By passing the query as the second argument, our useEffect() hook will only run when necessary.
useEffect(() => {
    fetchData().then(data => {
        setData(data);
        console.log(data);
    }
}, [query]);
Enter fullscreen mode Exit fullscreen mode


Full Sandbox: https://codesandbox.io/s/live-search-demo-dvzbz?file=/src/App.js

Async in JS

There are three ways to handle asynchronous functions in Javascript.

  1. Callbacks - es5

    fetchData(query, function(err, data) {
        if (err) {
            console.log(err);
        }
        setData(data);
    });
    
  2. Promises - es6

    fetchData(query)
        .then(data => setData(data))
        .catch(error => console.log(error));
    
  3. Async-await - es7

    try {
        const data = await fetchData(query);
        setData(data);
    catch (err) {
        console.log(err);
    }
    

As you can see, using promises and async await presents a much cleaner way to handle asynchronous operations. Personally I like async-await the most, because it allows you to write synchronous looking code. Internally JS uses generators to pause execution until whatever is "awaited" has finished execution before continuing on. There's a catch though, you can only use the await keyword within of an async function.

async function loadData() {
    const data = await fetchData(query);
    setData(data);
}

// also works with arrow functions
const loadData = async () => {
    const data = await fetchData(query);
    setData(data);
}
Enter fullscreen mode Exit fullscreen mode

Async functions and useEffect()

React's useEffect() hook doesn't allow for async functions to be directly passed to it.

// NOT ALLOWED
useEffect(async () => {
    const data = await fetchData(query);
    setData(data);
});
Enter fullscreen mode Exit fullscreen mode


This is because you have the option of returning a clean up function from the function passed to useEffect(). This is analogous to implementing componentWillUnmount() for class based components.

useEffect(() => {
    // do stuff...
    return () => {
        // do some kind of clean up
        someAPI.unsubscribe();
    }
});
Enter fullscreen mode Exit fullscreen mode


The problem with async functions is that instead of returning a clean up function, or undefined, it will return a promise. There are two ways to get around this limitation:

  1. Abstract it into an async function and call it

    useEffect(() => {
        async function loadData() {
            const data = await fetchData(query);
            setData(data);
        }
        loadData();
    });
    
  2. IIFE - declares a function and immediately executes it

    useEffect(() => {
        (async () => {
            const data = await fetchData(query);
            setData(data);
        })();
    });
    


Further Readings and Sources

Top comments (0)