Understanding React useEffect Hook


In React applications development, there are times when we may want some piece of code to be executed after a component has been rendered. For example, we may want some configuration settings loaded after the application starts execution. In another example, we may want to run some piece of code in response to a change in a specific state variable.

To execute some code logic outside React’s virtual DOM, we need to wait until React has rendered the component, and then invoke useEffect hook to run a function that executes the side effect code.

In this post, we will examine side effects, and the need to invoke useEffect hook to asynchronously run a function to execute a side effect code. We will also consider how we can control the execution of side effect code with dependencies.

Table of Contents

What are Side Effects?

React, as you may know, is a javascript library for building user interfaces. It is used for building visual elements in web and mobile applications. However, many useful applications are not complete with only UI elements. They may interact with resources that are outside of UI presentation.

For example, applications may need to interact with resources such as files, browsers DOM, localStorage, fetch data from databases and external API, etc. Such interactions with resources outside React are referred to as side effects. Such application codes outside React’s UI presentation are executed with useEffect hook.

useEffect Runs Side Effect Function

As ealier indicated, a React application may need to interact with resources outside of the UI code. We have referred to such external interactions as side effects. For any React component, if there is the need to execute side effects, we need to inform React about the function that needs to be called to interact with external resources.

React useEffect hook is used to call a function that executes outside of React’s virtual DOM. Thus, useEffect hook is used to call a function that runs side effect code. It accepts a mandatory function as the first argument and an optional dependency array as second argument. The code inside the function passed to the useEffect hook is the side effect code we intend to run. It has the following signature:

JavaScript
// useEffect function signature
useEffect(side_effect_function, [optional_dependency]);

The function which is called to execute side effect code can be a named function or an anonymous function. Executing side effect code with anonymous function is the most common practice. For example, the following code passes an anonymous function to useEffect:

JavaScript
// execute side effect code with anonymous function
useEffect(() => {
    document.title = 'Understanding useEffect Hook';
});

The alternative call, which is less common, is to pass the name of the function that executes side effect code. An example is shown below:

JavaScript
// execute side effect code with named function
useEffect(setTitle);

// side effect function
function setTitle() {
    document.title = 'Understanding useEffect Hook';
}

From the above code, it can be observed that setTitle() interacts with the browser’s document object. This document object is outside of React’s UI presentation. Since this is a side effect, we execute setTitle() with a useEffect call on line 2.

Parts of Side Effect Function

A side effect function, which is run by useEffect hook, has two parts. These are the side effect code and the cleanup code. The parts of the side effect function are explained below:

  1. Side Effect Code: This is the actual side effect code that is run by the function. It may include tasks such as fetching data from an API, connecting to a database, manipulating the browser’s DOM, interacting with the browsers localStorage, etc. In sum, this is the main task that the side effect function performs.
  2. Cleanup Code (Optional): This is the code to be executed when the component is about to be unmounted from the DOM. For example, if the side effect code performs tasks which involve allocating memory, registering events, or other resource usages, then this is the part where cleanup must be done. This part is where any allocated memory must be freed, registered events must be unregistered, etc.

Within the side effect function, the side effect code is mainly the code in the body of the side effect function excluding a return statement.

JavaScript
useEffect(() => {
    /*
     * the side effect code for the function somewhere here.
     * This is the part of the code with return statement excluded
     */
     document.title = 'Hello React';
});

From listing 4, the side effect code is the code in the body of the anonymous function passed to the useEffect hook. This is the actual task that we want the function to perform. In this example, the function sets the title of the HTML document. The side effect code does not include a return statement.

If there is the need to perform any cleanup, it must be done by another function. For React to know that a cleanup will need to be performed before the component unmounts, we will need to return the function that performs the cleanup from the side effect function passed to useEffect.

From the code in listing 5 below, the function called to perform side effect returns a function that will need to be called to perform cleanup:

JavaScript
useEffect(() => {
    // the side effect code for the function somewhere here
    
    /* when the component is about to be unmounted, the
     * return function below will be called to perform
     * any cleanup before component unmount is done
     */
    return () => {
        // perform cleanup, such as unregistering events, release 
        // of resources created and used in side effect, etc.
    };
});

We can observe that there is a return statement on line 8. As can be seen, line 8 returns a function. This is a cleanup function. When present, React will remember this function but will not call it immediately. This cleanup function will be called to perform any cleanup of resources allocated when React is about to unmount the component within which the side effect was performed.

Just as a return statement is optional in any javascript function, the cleanup function is also optional. This is because not all side effect codes use memory allocating resources or subscribe to some events. Such side effect codes do not require cleanup. In such cases, there is no need to return a cleanup function..

Since a cleanup function is optional, it implies that we can write a side effect code without returning a cleanup function. For example:

JavaScript
useEffect(() => {
    // interacting directly with the browser document is side effect
    document.title = 'React Applications Development';
});

Considering the above code, no cleanup is required to be done after the execution of the side effect code. Due to this, we do not return any cleanup function.

When Does useEffect Execute?

A useEffect hook is always executed when the component first mounts (first render), and optionally on component updates (re-render). This implies that a useEffect hook will be invoked at least once.

The first execution occurs after the component first renders. Whether it will execute again or not depends on some specified props and or state variables defined as the second argument, a dependency array.

To better understand when a useEffect hook usage, we will look at when side effect code executes and different ways in which we can control its subsequent execution.

1. Component Mount and All Updates

By default, a useEffect will always run when the component is rendered for the first time, and subsequently execute whenever the component updates. In other words, it will execute on all component renders, whether on first render (mount) or re-renders (updates). This is the default uncontrolled behaviour.

To execute a useEffect hook anytime on component mount and component updates, we will need to pass only the first argument, which is the side effect function, to useEffect hook. In this case, the second argument, which is the dependency array, should not be passed.

JavaScript
useEffect(side_effect_function);

As can be seen, we are only passing the side effect function as the first argument without passing the second argument to useEffect. An example is shown below:

JavaScript
useEffect(() => {
    // side effect code here
});

The following code is a working example of useEffect hook that runs a side effect function when the component is first mounted and when the component is updated.

JavaScript
// perform necessary imports
import React, {useState, useEffect} from "react";

const App = () => {
    // state variable for heading text
    const [heading, setHeading] = useState('Sample Text');

    // call the argument function to set the document title
    // when this App component first mounts in the DOM and 
    // anytime the App component is updated
    useEffect(() => {
        // set the title of the HTML document
        document.title = heading;
    });

    return (
        <div style={{textAlign: 'center'}}>
            <h2>{heading}</h2>
            <hr />

            <div style={{marginTop: '20px'}}>
                <label>Enter Text: </label>
                <input type='text' onChange={(e) => setHeading(e.target.value)} />
            </div>
        </div>
    )
}

export default App;

We have said that a useEffect hook will always run at least once: when the component in which it is contained is mounted for the first time.

Since a useEffect hook is executed after component mount, the HTML document title is set to the heading state variable, which by then has the value 'Sample Text'. Hence the title of the HTML document will be set to Sample Text when the document load is completed.

We have also indicated that subsequent execution of side effect code depends on the second argument passed to useEffect. Since we did not pass the second argument to the useEffect call, the side effect code will always be executed when the component updates.

On line 23, in listing 9, an onChange event handler, setHeading, updates state variable heading as we enter text in the input control. The heading state variable is also set to the <h2></h2> tag on line 18, causing the App component to update.

An update to the App component triggers the useEffect call to set the title of the document to the state variable heading. The result is that whenever we type in the input control, the entered text is updated in the h2 heading text, and is subsequently set as the HTML document title.

useeffect

As we type in the input control, the App component is re-rendered to update the heading text. However, because we did not pass any dependency value to useEffect, the side effect code is executed whenever the App component updates. Thus, the change in the heading text is also reflected in the change in the title of the HTML document.

2. Component Mount and Conditional State / Prop Update

Recall that the second argument passed to useEffect is a dependency information. It tells that, after the first execution on mount, any further execution of the side effect code depends on some condition or state of some specified variables. React allows us to specify multiple states or props which, when they change, trigger further execution of the side effect code.

We can specify the dependent state or prop variables as an array and pass this dependency array as the second argument to useEffect hook call:

JavaScript
useEffect(side_effect_function, [dependency_state_variables]);

In the above code, we have specified the second argument to useEffect as an array. The dependency array contains the state or prop variables that the side effect code will depend on for further execution. For example:

JavaScript
useEffect(() => {
    // side effect code here
}, [var1]);

After the first execution of the side effect code on component mount, further execution will only occur when var1 changes.

If there are multiple state or prop variables that the side effect code will depend on for further execution, they will need to be separated by comma in the array delimiter:

JavaScript
useEffect(() => {
    // side effect code here
}, [var1, var1, var3]);

From the above code listing, we are actually saying that, after the first execution on component mount, the side effect code should be executed again when a change occurs in either var1, var2, or var3. Thus, further execution of the side effect code depends on changes in the state or prop variables specified in the dependency array.

To demonstrate with an example, we will consider one similar to the previous demonstration. Rather than update the title of the HTML document as we type in the input control, we will rather update only the content heading. The HTML document title will be updated to the entered heading text after a button is clicked.

JavaScript
// perform necessary imports
import React, {useState, useEffect} from "react";

const App = () => {
    // state variable for heading text
    const [heading, setHeading] = useState('Sample Text');
    const [canSetTitle, setCanSetTitle] = useState(false);

    // call the argument function to set the document title
    // when this App component first mounts in the DOM and 
    // anytime the App component is updated
    useEffect(() => {
        // set the title of the HTML document
        document.title = canSetTitle ? heading : document.title;
    }, [canSetTitle]);

    const onInputChange = (e) => {
        setHeading(e.target.value);
        setCanSetTitle(false);
    }
    
    return (
        <div style={{textAlign: 'center'}}>
            <h2>{heading}</h2>
            <hr />

            <div style={{marginTop: '20px'}}>
                <label>Enter Text: </label>
                <input type='text' onChange={onInputChange} />
                <button onClick={() => { setCanSetTitle(true) }}>Set Title</button>
            </div>
        </div>
    )
}

export default App;

On line 7, we declare boolean state variable canSetTitle, which we will use to determine whether we can set the title of the HTML document or not. Since this is a side effect, we execute with a useEffect call on line 12.

The side effect code will be executed when the App component first mounts. However its subsequent execution will be done when canSetTitle state variable changes. This is because we have passed canSetTitle state variable as a dependency in the array passed as second argument to the the useEffect call.

As we type in the input control, the onInputChange event handler is called to set the content heading. This time, the document title is not set automatically. The onInputChange event handler rather sets canSetTitle state variable to false.

A click on the button updates canSetTitle to true. This update in the state variable, set as a dependency on line 15, causes the side effect code to execute. If canSetTitle is true, then the document title is set to the heading text. However, if canSetTitle is false, the title is maintained.

As we type in the input control, the content heading updates. However, the title of the HTML document does not update until the button is clicked. This is because the side effect code which updates the HTML document title depends on changes in the state variable canSetTitle.

3. Component Mount Only

Sometimes, we may want to execute side effect code only once when a component is mounted (first render). In this case, we wouldn’t want the side effect code to be executed any further. For example, after an App component is mounted, we may want to load some configuration settings from an external server. Once the configuration settings are loaded, we may not want to load the same configuration settings again just because there is a UI update.

To execute a side effect code only once, we will need to pass an empty array as the second argument to the useEffect hook call:

JavaScript
useEffect(() => {
    // side effect code here
}, []);

The side effect code will be executed when the component is first mounted. However, because the dependency array is empty, there wouldn’t be any prop or state variable to trigger further execution. The result is that the side effect code will never be executed again.

The following code listing is a slight update to listing 13 seen earlier. For brevity, let’s see line 12 to line 15 in listing 13, shown below:

JavaScript
useEffect(() => {
    // set the title of the HTML document
    document.title = canSetTitle ? heading : document.title;
}, [canSetTitle]);

The only update we will do is to clear canSetTitle from the dependency array. This will mean that we will have an empty array as the second argument to the useEffect hook call.

JavaScript
useEffect(() => {
    // set the title of the HTML document
    document.title = canSetTitle ? heading : document.title;
}, []);

Every other part of listing 13 must be left intact. The complete code after the performing the update is shown below:

JavaScript
// perform necessary imports
import React, {useState, useEffect} from "react";

const App = () => {
    // state variable for heading text
    const [heading, setHeading] = useState('Sample Text');
    const [canSetTitle, setCanSetTitle] = useState(false);

    // call the argument function to set the document title
    // when this App component first mounts in the DOM and 
    // anytime the App component is updated
    useEffect(() => {
        // set the title of the HTML document
        document.title = canSetTitle ? heading : document.title;
    }, []);

    const onInputChange = (e) => {
        setHeading(e.target.value);
        setCanSetTitle(false);
    }
    
    return (
        <div style={{textAlign: 'center'}}>
            <h2>{heading}</h2>
            <hr />

            <div style={{marginTop: '20px'}}>
                <label>Enter Text: </label>
                <input type='text' onChange={onInputChange} />
                <button onClick={() => { setCanSetTitle(true) }}>Set Title</button>
            </div>
        </div>
    )
}

export default App;

Compared to listing 13, the code listing above has an empty array passed to the useEffect hook call on line 15. With empty array dependency, the side effect code will not execute again after its first execution on component mount.

useeffect-mount-only

As we type in the input control, the content heading updates. However, the document title does not change after clicking on the button. This is because the useEffect hook call has an empty dependency array. Therefore, the side effect code does not execute when there is component update (re-render).

Summary

A React application may interact with code or resources that are outside React’s UI presentation. Such interactions are known as side effects. They include interactions such as connection with external API, browser DOM, localStorage, etc.

To execute a side effect code, the React useEffect hook is invoked to run the function that executes side effect code. The side effect function may optionally return another function that will be called to perform side effect cleanups when the component is about to be unmounted from the DOM.

React useEffect will always run a side effect function when the component is first mounted in the DOM. Its subsequent execution depends on a second argument passed to useEffect. This second argument is specified as an array of state or prop variables whose updates trigger the further execution of the side effect code.

If an empty array is passed as dependency, then the useEffect hook call will never be run again. If state or prop variables are specified, then the side effect code will execute when any of the specified state or prop variables are updated.


Leave a Reply

Your email address will not be published. Required fields are marked *