The author selected Creative Commons to receive a donation as part of the Write for DOnations program.
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.
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.
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:
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:
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:
.wrapper {
padding: 20px
}
Save and close the file. When you do, the browser will refresh and render the basic components.
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:
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:
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:
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:
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
:
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:
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:
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:
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:
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.
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:
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.
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:
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
:
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.
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.
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
:
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:
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.
If you navigate to the Network tab in Chrome or Firefox, you’ll find that the code is broken into different 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:
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.
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.
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
React is a popular JavaScript framework for creating front-end applications, such as user interfaces that allow users to interact with programs. Originally created by Facebook, it has gained popularity by allowing developers to create fast applications using an intuitive programming paradigm that ties JavaScript with an HTML-like syntax known as JSX.
In this series, you will build out examples of React projects to gain an understanding of this framework, giving you the knowledge you need to pursue front-end web development or start out on your way to full stack development.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
I chanced upon and started practicing your tutorials beginning Dec 25, 2021. Amazing how I would have gotten some jobs had I these resources.
Now I’m confident in React and thanks for the free ebook
Joe Morgan, thanks for the awesome article.
I shared it with my team as it makes data loading, lazy loading, and code splitting in react really digestible!