When you need to execute asynchronous code in a React component, that usually involves a Javascript promise. Making sure your component updates when a promise completes isn't entirely obvious at first, so in this guide I will show you how to work with promises in a React component.
When creating components in React, there are two options: class components or functional components. Using a class component means your component will automatically have the idea of state
, as well as component lifecycle methods.
1import React from "react";
2
3class UserComponent extends React.Component {
4 constructor(props) {
5 super(props);
6 this.state = {
7 email: "",
8 password: ""
9 };
10 }
11
12 componentDidMount() {}
13
14 componentShouldUpdate() {}
15
16 componentWillUnmount() {}
17
18 render() {
19 return (
20 <div>
21 <h1>A user</h1>
22 </div>
23 );
24 }
25}
The component lifecycle methods are important because they are where you'll want to handle asynchronous code. Specifically, the componentDidMount
method can be used to update your component with new state when a promise resolves. Here's how to do that:
1import React from "react";
2
3class UserComponent extends React.Component {
4 constructor(props) {
5 super(props);
6 this.state = {
7 email: "",
8 password: ""
9 };
10 }
11
12 componentDidMount() {
13 const fetchUserEmail = async () => {
14 const response = await fetch("/emails");
15 const { email } = await response.json();
16 this.setState({
17 email
18 });
19 };
20 fetchUserEmail();
21 }
22
23 render() {
24 return (
25 <div>
26 <h1>A user</h1>
27 <p>{this.state.email}</p>
28 </div>
29 );
30 }
31}
In the above code snippet I am fetching emails from an imaginary endpoint that returns an object with an email
property. I am using the async/await
syntax to handle the promise that fetch
returns, as well as the promise returned by calling json()
on the response. I destructured the email
property from the resulting json and I call this.setState
to make sure we update the component with the new value for this.state.email
.
It's not required to use componentDidMount
to handle promises. Use componentDidMount
when you only want the promise to execute a single time as the component loads. You could execute the same promise when a use clicks a button. For example:
1import React from "react";
2
3class UserComponent extends React.Component {
4 constructor(props) {
5 super(props);
6 this.state = {
7 email: "",
8 password: ""
9 };
10 }
11
12 handleButtonClick = () => {
13 const fetchUserEmail = async () => {
14 const response = await fetch("/emails");
15 const { email } = await response.json();
16 this.setState({
17 email
18 });
19 };
20 fetchUserEmail();
21 };
22
23 render() {
24 return (
25 <div>
26 <h1>A user</h1>
27 <p>{this.state.email}</p>
28 <button onClick={this.handleButtonClick}>Fetch Email</button>
29 </div>
30 );
31 }
32}
If instead of using a class component you go with a functional component, the process is very similar. The biggest differences are in how lifecycle is handled and how to use state. Functional components don't have lifecycle methods, but they do let you control when a component updates. Instead of thinking, "How do I replicate updating a component when it mounts?" we can think, "How do I update the component when some value changes?" For this functionality, we'll use the useEffect
hook. There is no this.setState
in a functional component, either. We are able to use the useState
hook to give your component state. Here's how that would look:
1import React from "react";
2
3const UserComponent = props => {
4 const [email, setEmail] = React.useState("");
5 React.useEffect(() => {
6 const fetchUserEmail = async () => {
7 const response = await fetch("/emails");
8 const { email } = await response.json();
9 setEmail(email);
10 };
11 fetchUserEmail();
12 }, []);
13
14 return (
15 <div>
16 <h1>A user</h1>
17 <p>{email}</p>
18 </div>
19 );
20};
The useEffect
hook takes two arguments: a function as the first argument and a dependency array as the second. The dependency array takes a list values, and when any of those values changes, the function you passed as the first argument will run. If you don't pass any values to the dependency array, the function will only run once when the component loads. This is similar behavior to the componentDidMount
lifecycle method.
You are creating a state variable called email
with this line:
1const [email, setEmail] = React.useState("");
useState
always returns an array with two values: the current state value and an updater function, always in that order. This means we can destructure that array as we are doing above. Passing the empty string to useState
means we are defaulting the value of email
to an empty string. Calling setEmail
is just like calling this.setState
in a class component. The component will rerender with the new email
value.
Similar to the class component example, you don't always have to handle promises in the useEffect
hook.
1import React from "react";
2
3const UserComponent = props => {
4 const [email, setEmail] = React.useState("");
5
6 const fetchUserEmail = async () => {
7 const response = await fetch("/emails");
8 const { email } = await response.json();
9 setEmail(email);
10 };
11
12 return (
13 <div>
14 <h1>A user</h1>
15 <p>{email}</p>
16 <button onClick={fetchUserEmail}>Fetch Email</button>
17 </div>
18 );
19};
In the above example, we are firing off the fetch
promise on click of the "Fetch Email" button. When the promise resolves, we'll call setEmail
with the new email address.
There are slight differences when working with promises between class components and functional components. In this guide, we covered two common situations: handling promises on component mount and handling promises when a user takes an action. Now that you learned how to use them, I hope you try adding promises to your next project 😊.
Checkout my course Javascript Generators and Iterators to learn more about the Javascript core language.