How To Handle Async Data Loading, Lazy Loading, and Code Splitting with React

The author selected Creative Commons to receive a donation as part of the Write for DOnations program.

Introduction

As a JavaScript web developer, asynchronous code gives you the ability to run some parts of your code while other parts are still waiting for data or resolving. This means that important parts of your app will not have to wait for less important parts before they render. With asynchronous code you can also update your application by requesting and displaying new information, giving users a smooth experience even when long functions and requests are processing in the background.

In React development, asynchronous programming presents unique problems. When you use React functional components for example, asynchronous functions can create infinite loops. When a component loads, it can start an asynchronous function, and when the asynchronous function resolves it can trigger a re-render that will cause the component to recall the asynchronous function. This tutorial will explain how to avoid this with a special Hook called useEffect, which will run functions only when specific data changes. This will let you run your asynchronous code deliberately instead of on each render cycle.

Asynchronous code is not just limited to requests for new data. React has a built-in system for lazy loading components, or loading them only when the user needs them. When combined with the default webpack configuration in Create React App, you can split up your code, reducing a large application into smaller pieces that can be loaded as needed. React has a special component called Suspense that will display placeholders while the browser is loading your new component. In future versions of React, you’ll be able to use Suspense to load data in nested components without render blocking.

In this tutorial, you’ll handle asynchronous data in React by creating an app that displays information on rivers and simulates requests to Web APIs with setTimeout. By the end of this tutorial, you’ll be able to load asynchronous data using the useEffect Hook. You’ll also be able to safely update the page without creating errors if the component unmounts before data resolution. Finally, you’ll split a large application into smaller parts using code splitting.

Prerequisites

  • You will need a development environment running Node.js; this tutorial was tested on Node.js version 10.20.1 and npm version 6.14.4. To install this on macOS or Ubuntu 18.04, follow the steps in How to Install Node.js and Create a Local Development Environment on macOS or the Installing Using a PPA section of How To Install Node.js on Ubuntu 18.04.

  • A React development environment set up with Create React App, with the non-essential boilerplate removed. To set this up, follow Step 1 — Creating an Empty Project of the How To Manage State on React Class Components tutorial. This tutorial will use async-tutorial as the project name.

  • You will be using React events and Hooks, including the useState and the useReducer Hooks. You can learn about events in our How To Handle DOM and Window Events with React tutorial, and Hooks at How to Manage State with Hooks on React Components.

  • You will also need a basic knowledge of JavaScript and HTML, which you can find in our How To Build a Website with HTML series and in How To Code in JavaScript. Basic knowledge of CSS would also be useful, which you can find at the Mozilla Developer Network.

Step 1 — Loading Asynchronous Data with useEffect

In this step, you’ll use the useEffect Hook to load asynchronous data into a sample application. You’ll use the Hook to prevent unnecessary data fetching, add placeholders while the data is loading, and update the component when the data resolves. By the end of this step, you’ll be able to load data with useEffect and set data using the useState Hook when it resolves.

To explore the topic, you are going to create an application to display information about the longest rivers in the world. You’ll load data using an asynchronous function that simulates a request to an external data source.

First, create a component called RiverInformation. Make the directory:

  • mkdir src/components/RiverInformation

Open RiverInformation.js in a text editor:

  • nano src/components/RiverInformation/RiverInformation.js

Then add some placeholder content:

async-tutorial/src/components/RiverInformation/RiverInformation.js

import React from 'react';  export default function RiverInformation() {   return(     <div>       <h2>River Information</h2>     </div>   ) } 

Save and close the file. Now you need to import and render the new component to your root component. Open App.js:

  • nano src/components/App/App.js

Import and render the component by adding in the highlighted code:

async-tutorial/src/components/App/App.js

import React from 'react'; import './App.css'; import RiverInformation from '../RiverInformation/RiverInformation';  function App() {   return (     <div className="wrapper">       <h1>World's Longest Rivers</h1>       <RiverInformation />     </div>   ); }  export default App; 

Save and close the file.

Finally, in order to make the app easier to read, add some styling. Open App.css:

  • nano src/components/App/App.css

Add some padding to the wrapper class by replacing the CSS with the following:

async-tutorial/src/components/App/App.css

.wrapper {     padding: 20px } 

Save and close the file. When you do, the browser will refresh and render the basic components.

Basic Component, 1

In this tutorial, you’ll make generic services for returning data. A service refers to any code that can be reused to accomplish a specific task. Your component doesn’t need to know how the service gets its information. All it needs to know is that the service will return a Promise. In this case, the data request will be simulated with setTimeout, which will wait for a specified amount of time before providing data.

Create a new directory called services under the src/ directory:

  • mkdir src/services

This directory will hold your asynchronous functions. Open a file called rivers.js:

  • nano src/services/rivers.js

Inside the file, export a function called getRiverInformation that returns a promise. Inside the promise, add a setTimeout function that will resolve the promise after 1500 milliseconds. This will give you some time to see how the component will render while waiting for data to resolve:

async-tutorial/src/services/rivers.js

export function getRiverInformation() {   return new Promise((resolve) => {     setTimeout(() => {       resolve({         continent: 'Africa',         length: '6,650 km',         outflow: 'Mediterranean'       })     }, 1500)   }) } 

In this snippet, you are hard-coding the river information, but this function will be similar to any asynchronous functions you may use, such as an API call. The important part is that the code returns a promise.

Save and close the file.

Now that you have a service that returns the data, you need to add it to your component. This can sometimes lead to a problem. Suppose you called the asynchronous function inside of your component and then set the data to a variable using the useState Hook. The code will be like this:

import React, { useState } from 'react'; import { getRiverInformation } from '../../services/rivers';  export default function RiverInformation() {   const [riverInformation, setRiverInformation] = useState({});    getRiverInformation()   .then(d => {     setRiverInformation(d)   })    return(     ...   ) } 

When you set the data, the Hook change will trigger a components re-render. When the component re-renders, the getRiverInformation function will run again, and when it resolves it will set the state, which will trigger another re-render. The loop will continue forever.

To solve this problem, React has a special Hook called useEffect that will only run when specific data changes.

The useEffect Hook accepts a function as the first argument and an array of triggers as the second argument. The function will run on the first render after the layout and paint. After that, it will only run if one of the triggers changes. If you supply an empty array, it will only run one time. If you do not include an array of triggers, it will run after every render.

Open RiverInformation.js:

  • nano src/components/RiverInformation/RiverInformation.js

Use the useState Hook to create a variable called riverInformation and a function called setRiverInformation. You’ll update the component by setting the riverInformation when the asynchronous function resolves. Then wrap the getRiverInformation function with useEffect. Be sure to pass an empty array as a second argument. When the promise resolves, update the riverInformation with the setRiverInformation function:

async-tutorial/src/components/RiverInformation/RiverInformation.js

import React, { useEffect, useState } from 'react'; import { getRiverInformation } from '../../services/rivers';  export default function RiverInformation() {   const [riverInformation, setRiverInformation] = useState({});    useEffect(() => {    getRiverInformation()    .then(data =>      setRiverInformation(data)    );   }, [])     return(     <div>       <h2>River Information</h2>       <ul>         <li>Continent: {riverInformation.continent}</li>         <li>Length: {riverInformation.length}</li>         <li>Outflow: {riverInformation.outflow}</li>       </ul>     </div>   ) } 

After the asynchronous function resolves, update an unordered list with the new information.

Save and close the file. When you do the browser will refresh and you’ll find the data after the function resolves:

River Information Updating After Load, 2

Notice that the component renders before the data is loaded. The advantage with asynchronous code is that it won’t block the initial render. In this case, you have a component that shows the list without any data, but you could also render a spinner or a scalable vector graphic (SVG) placeholder.

There are times when you’ll only need to load data once, such as if you are getting user information or a list of resources that never change. But many times your asynchronous function will require some arguments. In those cases, you’ll need to trigger your use useEffect Hook whenever the data changes.

To simulate this, add some more data to your service. Open rivers.js:

  • nano src/services/rivers.js

Then add an object that contains data for a few more rivers. Select the data based on a name argument:

async-tutorial/src/services/rivers.js

const rivers = {  nile: {    continent: 'Africa',    length: '6,650 km',    outflow: 'Mediterranean'  },  amazon: {    continent: 'South America',    length: '6,575 km',    outflow: 'Atlantic Ocean'  },  yangtze: {    continent: 'Asia',    length: '6,300 km',    outflow: 'East China Sea'  },  mississippi: {    continent: 'North America',    length: '6,275 km',    outflow: 'Gulf of Mexico'  } }  export function getRiverInformation(name) {   return new Promise((resolve) => {     setTimeout(() => {       resolve(         rivers[name]       )     }, 1500)   }) } 

Save and close the file. Next, open App.js so you can add more options:

  • nano src/components/App/App.js

Inside App.js, create a stateful variable and function to hold the selected river with the useState Hook. Then add a button for each river with an onClick handler to update the selected river. Pass the river to RiverInformation using a prop called name:

async-tutorial/src/components/App/App.js

import React, { useState } from 'react'; import './App.css'; import RiverInformation from '../RiverInformation/RiverInformation';  function App() {   const [river, setRiver] = useState('nile');   return (     <div className="wrapper">       <h1>World's Longest Rivers</h1>       <button onClick={() => setRiver('nile')}>Nile</button>       <button onClick={() => setRiver('amazon')}>Amazon</button>       <button onClick={() => setRiver('yangtze')}>Yangtze</button>       <button onClick={() => setRiver('mississippi')}>Mississippi</button>       <RiverInformation name={river} />     </div>   ); }  export default App; 

Save and close the file. Next, open RiverInformation.js:

  • nano src/components/RiverInformation/RiverInformation.js

Pull in the name as a prop and pass it to the getRiverInformation function. Be sure to add name to the array for useEffect, otherwise it will not rerun:

async-tutorial/src/components/RiverInformation/RiverInformation.js

import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { getRiverInformation } from '../../services/rivers';  export default function RiverInformation({ name }) {   const [riverInformation, setRiverInformation] = useState({});    useEffect(() => {     getRiverInformation(name)     .then(data =>       setRiverInformation(data)     );   }, [name])     return(     <div>       <h2>River Information</h2>       <ul>         <li>Continent: {riverInformation.continent}</li>         <li>Length: {riverInformation.length}</li>         <li>Outflow: {riverInformation.outflow}</li>       </ul>     </div>   ) }  RiverInformation.propTypes = {  name: PropTypes.string.isRequired } 

In this code, you also added a weak typing system with PropTypes, which will make sure that the prop is a string.

Save the file. When you do, the browser will refresh and you can select different rivers. Notice the delay between when you click and when the data renders:

Update river information, 3

If you had left out the name prop from the useEffect array, you would receive a build error in the browser console. It would be something like this:

ErrorCompiled with warnings.  ./src/components/RiverInformation/RiverInformation.js   Line 13:6: React Hook useEffect has a missing dependency: 'name'. Either include it or remove the dependency array react-hooks/exhaustive-deps  Search for the keywords to learn more about each warning. To ignore, add // eslint-disable-next-line to the line before. 

This error tells you that the function in your effect has dependencies that you are not explicitly setting. In this situation, it’s clear that the effect wouldn’t work, but there are times when you may be comparing prop data to stateful data inside the component, which makes it possible to lose track of items in the array.

The last thing to do is to add some defensive programming to your component. This is a design principle that emphasizes high availability for your application. You want to ensure that your component will render even if the data is not in the correct shape or if you do not get any data at all from an API request.

As your app is now, the effect will update the riverInformation with any type of data it receives. This will usually be an object, but in cases where it’s not, you can use optional chaining to ensure that you will not throw an error.

Inside RiverInformation.js, replace the instance of an object dot chaining with optional chaining. To test if it works, remove the default object {} from the useState function:

async-tutorial/src/components/RiverInformation/RiverInformation.js

import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { getRiverInformation } from '../../services/rivers';  export default function RiverInformation({ name }) {   const [riverInformation, setRiverInformation] = useState();    useEffect(() => {     getRiverInformation(name)     .then(data =>       setRiverInformation(data)     );   }, [name])    return(     <div>       <h2>River Information</h2>       <ul>         <li>Continent: {riverInformation?.continent}</li>         <li>Length: {riverInformation?.length}</li>         <li>Outflow: {riverInformation?.outflow}</li>       </ul>     </div>   ) }  RiverInformation.propTypes = {   name: PropTypes.string.isRequired } 

Save and close the file. When you do, the file will still load even though the code is referencing properties on undefined instead of an object:

River Information Updating After Load, 4

Defensive programming is usually considered a best practice, but it’s especially important on asynchronous functions such as API calls when you can’t guarantee a response.

In this step, you called asynchronous functions in React. You used the useEffect Hook to fetch information without triggering re-renders and triggered a new update by adding conditions to the useEffect array.

In the next step, you’ll make some changes to your app so that it updates components only when they are mounted. This will help your app avoid memory leaks.

Step 2 — Preventing Errors on Unmounted Components

In this step, you’ll prevent data updates on unmounted components. Since you can never be sure when data will resolve with asynchronous programming, there’s always a risk that the data will resolve after the component has been removed. Updating data on an unmounted component is inefficient and can introduce memory leaks in which your app is using more memory than it needs to.

By the end of this step, you’ll know how to prevent memory leaks by adding guards in your useEffect Hook to update data only when the component is mounted.

The current component will always be mounted, so there’s no chance that the code will try and update the component after it is removed from the DOM, but most components aren’t so reliable. They will be added and removed from the page as the user interacts with the application. If a component is removed from a page before the asynchronous function resolves, you can have a memory leak.

To test out the problem, update App.js to be able to add and remove the river details.

Open App.js:

  • nano src/components/App/App.js

Add a button to toggle the river details. Use the useReducer Hook to create a function to toggle the details and a variable to store the toggled state:

async-tutorial/src/components/App/App.js

import React, { useReducer, useState } from 'react'; import './App.css'; import RiverInformation from '../RiverInformation/RiverInformation';  function App() {   const [river, setRiver] = useState('nile');   const [show, toggle] = useReducer(state => !state, true);   return (     <div className="wrapper">       <h1>World's Longest Rivers</h1>       <div><button onClick={toggle}>Toggle Details</button></div>       <button onClick={() => setRiver('nile')}>Nile</button>       <button onClick={() => setRiver('amazon')}>Amazon</button>       <button onClick={() => setRiver('yangtze')}>Yangtze</button>       <button onClick={() => setRiver('mississippi')}>Mississippi</button>       {show && <RiverInformation name={river} />}     </div>   ); }  export default App; 

Save the file. When you do the browse will reload and you’ll be able to toggle the details.

Click on a river, then immediately click on the Toggle Details button to hide details. React will generate an error warning that there is a potential memory leak.

Warning when component is updated after being removed, 5

To fix the problem you need to either cancel or ignore the asynchronous function inside useEffect. If you are using a library such as RxJS, you can cancel an asynchronous action when the component unmounts by returning a function in your useEffect Hook. In other cases, you’ll need a variable to store the mounted state.

Open RiverInformation.js:

  • nano src/components/RiverInformation/RiverInformation.js

Inside the useEffect function, create a variable called mounted and set it to true. Inside the .then callback, use a conditional to set the data if mounted is true:

async-tutorial/src/components/RiverInformation/RiverInformation.js

 import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { getRiverInformation } from '../../services/rivers';  export default function RiverInformation({ name }) {   const [riverInformation, setRiverInformation] = useState();    useEffect(() => {     let mounted = true;     getRiverInformation(name)     .then(data => {       if(mounted) {         setRiverInformation(data)       }     });   }, [name])     return(     <div>       <h2>River Information</h2>       <ul>         <li>Continent: {riverInformation?.continent}</li>         <li>Length: {riverInformation?.length}</li>         <li>Outflow: {riverInformation?.outflow}</li>       </ul>     </div>   ) }  RiverInformation.propTypes = {   name: PropTypes.string.isRequired } 

Now that you have the variable, you need to be able to flip it when the component unmounts. With the useEffect Hook, you can return a function that will run when the component unmounts. Return a function that sets mounted to false:

async-tutorial/src/components/RiverInformation/RiverInformation.js

 import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { getRiverInformation } from '../../services/rivers';  export default function RiverInformation({ name }) {   const [riverInformation, setRiverInformation] = useState();    useEffect(() => {     let mounted = true;     getRiverInformation(name)     .then(data => {       if(mounted) {         setRiverInformation(data)       }     });     return () => {      mounted = false;    }   }, [name])    return(     <div>       <h2>River Information</h2>       <ul>         <li>Continent: {riverInformation?.continent}</li>         <li>Length: {riverInformation?.length}</li>         <li>Outflow: {riverInformation?.outflow}</li>       </ul>     </div>   ) }  RiverInformation.propTypes = {   name: PropTypes.string.isRequired } 

Save the file. When you do, you’ll be able to toggle the details without an error.

No warning when toggling, 6

When you unmount, the component useEffect updates the variable. The asynchronous function will still resolve, but it won’t make any changes to unmounted components. This will prevent memory leaks.

In this step, you made your app update state only when a component is mounted. You updated the useEffect Hook to track if the component is mounted and returned a function to update the value when the component unmounts.

In the next step, you’ll asynchronously load components to split code into smaller bundles that a user will load as needed.

Step 3 — Lazy Loading a Component with Suspense and lazy

In this step, you’ll split your code with React Suspense and lazy. As applications grow, the size of the final build grows with it. Rather than forcing users to download the whole application, you can split the code into smaller chunks. React Suspense and lazy work with webpack and other build systems to split your code into smaller pieces that a user will be able to load on demand. In the future, you will be able to use Suspense to load a variety of data, including API requests.

By the end of this step, you’ll be able to load components asynchronously, breaking large applications into smaller, more focused chunks.

So far you’ve only worked with asynchronously loading data, but you can also asynchronously load components. This process, often called code splitting, helps reduce the size of your code bundles so your users don’t have to download the full application if they are only using a portion of it.

Most of the time, you import code statically, but you can import code dynamically by calling import as a function instead of a statement. The code would be something like this:

import('my-library') .then(library => library.action()) 

React gives you an additional set of tools called lazy and Suspense. React Suspense will eventually expand to handle data loading, but for now you can use it to load components.

Open App.js:

  • nano src/components/App/App.js

Then import lazy and Suspense from react:

async-tutorial/src/components/App/App.js

import React, { lazy, Suspense, useReducer, useState } from 'react'; import './App.css'; import RiverInformation from '../RiverInformation/RiverInformation';  function App() {   const [river, setRiver] = useState('nile');   const [show, toggle] = useReducer(state => !state, true);   return (     <div className="wrapper">       <h1>World's Longest Rivers</h1>       <div><button onClick={toggle}>Toggle Details</button></div>       <button onClick={() => setRiver('nile')}>Nile</button>       <button onClick={() => setRiver('amazon')}>Amazon</button>       <button onClick={() => setRiver('yangtze')}>Yangtze</button>       <button onClick={() => setRiver('mississippi')}>Mississippi</button>       {show && <RiverInformation name={river} />}     </div>   ); }  export default App; 

lazy and Suspsense have two distinct jobs. You use the lazy function to dynamically import the component and set it to a variable. Suspense is a built-in component you use to display a fallback message while the code is loading.

Replace import RiverInformation from '../RiverInformation/RiverInformation'; with a call to lazy. Assign the result to a variable called RiverInformation. Then wrap {show && <RiverInformation name={river} />} with the Suspense component and a <div> with a message of Loading Component to the fallback prop:

async-tutorial/src/components/App/App.js

import React, { lazy, Suspense, useReducer, useState } from 'react'; import './App.css'; const RiverInformation = lazy(() => import('../RiverInformation/RiverInformation'));  function App() {   const [river, setRiver] = useState('nile');   const [show, toggle] = useReducer(state => !state, true);   return (     <div className="wrapper">       <h1>World's Longest Rivers</h1>       <div><button onClick={toggle}>Toggle Details</button></div>       <button onClick={() => setRiver('nile')}>Nile</button>       <button onClick={() => setRiver('amazon')}>Amazon</button>       <button onClick={() => setRiver('yangtze')}>Yangtze</button>       <button onClick={() => setRiver('mississippi')}>Mississippi</button>       <Suspense fallback={<div>Loading Component</div>}>         {show && <RiverInformation name={river} />}       </Suspense>     </div>   ); }  export default App; 

Save the file. When you do, reload the page and you’ll find that the component is dynamically loaded. If you want to see the loading message, you can throttle the response in the Chrome web browser.

Component Loading

If you navigate to the Network tab in Chrome or Firefox, you’ll find that the code is broken into different chunks.

Chunks

Each chunk gets a number by default, but with Create React App combined with webpack, you can set the chunk name by adding a comment by the dynamic import.

In App.js, add a comment of /* webpackChunkName: "RiverInformation" */ inside the import function:

async-tutorial/src/components/App/App.js

import React, { lazy, Suspense, useReducer, useState } from 'react'; import './App.css'; const RiverInformation = lazy(() => import(/* webpackChunkName: "RiverInformation" */ '../RiverInformation/RiverInformation'));  function App() {   const [river, setRiver] = useState('nile');   const [show, toggle] = useReducer(state => !state, true);   return (     <div className="wrapper">       <h1>World's Longest Rivers</h1>       <div><button onClick={toggle}>Toggle Details</button></div>       <button onClick={() => setRiver('nile')}>Nile</button>       <button onClick={() => setRiver('amazon')}>Amazon</button>       <button onClick={() => setRiver('yangtze')}>Yangtze</button>       <button onClick={() => setRiver('mississippi')}>Mississippi</button>       <Suspense fallback={<div>Loading Component</div>}>         {show && <RiverInformation name={river} />}       </Suspense>     </div>   ); }  export default App; 

Save and close the file. When you do, the browser will refresh and the RiverInformation chunk will have a unique name.

River Information Chunk

In this step, you asynchronously loaded components. You used lazy and Suspense to dynamically import components and to show a loading message while the component loads. You also gave custom names to webpack chunks to improve readability and debugging.

Conclusion

Asynchronous functions create efficient user-friendly applications. However, their advantages come with some subtle costs that can evolve into bugs in your program. You now have tools that will let you split large applications into smaller pieces and load asynchronous data while still giving the user a visible application. You can use the knowledge to incorporate API requests and asynchronous data manipulations into your applications creating fast and reliable user experiences.

If you would like to read more React tutorials, check out our React Topic page, or return to the How To Code in React.js series page.