When working with asynchronous requests and setting state in React, you can encounter some interesting side-effects. What happens if the request succeeds before the state is set? The reverse? With situations like this it’s hard to be confident in what the logic flow will look like. setState solves for this via allowing callback functions to be defined.

A typical response to create a new User record inside a React component might look something like this:

createUser = (event) => {
  fetch(`/users`, {
      method: 'post',
      body: JSON.stringify(event)
    })
    .then(response => response.json())
    .then(json => {
      this.setState({ currentUserId: json.id });
    })
}

render() {
  return (
    <div className="MyComponent">
      <form onSubmit={this.createUser}>
        <input type="text" name="userName" />
        <button type="submit" />
      </form>
    </div>
  );
}

When the form is submitted, a request to create a new User is sent. Once the request completes the stateful key currentUserId is updated.

This works well for the current setup. However, what if we want to immediately do something with the currentUserId.

createUser = (event) => {
  fetch(`/users`, {
      method: 'post',
      body: JSON.stringify(event)
    })
    .then(response => response.json())
    .then(json => {
      this.setState({ currentUserId: json.id });
    });

    this.sendWelcomeEmail();
}

sendWelcomeEmail = () => {
  fetch(`/mailers/welcome/${this.state.currentUserId}`)
    .then(response => response.json())
    .then(json => {
      console.log(`Mail sent to: ${this.state.currentUserId}`);
    })
}

This won’t work because the initial POST request to create the User is asynchronous and our call to sendWelcomeEmail is at the end of the createUser function. That means that sendWelcomeEmail will execute possibly before the fetch request returns and this.state.currentUserId will be null.

Ok, so easy fix right. Just move the sendWelcomeEmail into the fetch request.

createUser = (event) => {
  fetch(`/users`, {
      method: 'post',
      body: JSON.stringify(event)
    })
    .then(response => response.json())
    .then(json => {
      this.setState({ currentUserId: json.id });
      this.sendWelcomeEmail(); // moved inside the .then
    });
}

sendWelcomeEmail = () => {
  fetch(`/mailers/welcome/${this.state.currentUserId}`)
    .then(response => response.json())
    .then(json => {
      console.log(`Mail sent to: ${this.state.currentUserId}`);
    })
}

Now our sendWelcomeEmail is properly waiting until the response from fetch is returned but it still isn’t working because this.state.currentUserId is null. When I first encountered this it had me scratching my head. Until I read the following on reactjs.org.

React may batch multiple setState() calls into a single update for performance. Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.

So, calling setState and then directly relying on this.state variables is going to lead to frustration. Luckily, setState allows for callback functions to be supplied which is where our implementation comes in below.

createUser = (event) => {
  fetch(`/users`, {
      method: 'post',
      body: JSON.stringify(event)
    })
    .then(response => response.json())
    .then(json => {
      this.setState(
        { currentUserId: json.id },
        this.sendWelcomeEmail
      );
    });
}

sendWelcomeEmail = () => {
  fetch(`/mailers/welcome/${this.state.currentUserId}`)
    .then(response => response.json())
    .then(json => {
      console.log(`Mail sent to: ${this.state.currentUserId}`);
    })
}

Now our sendWelcomeEmail function waits for the fetch request to complete and also executes as a callback to the state being set for the currentUserId. Here’s the portion of code I’m talking about:

this.setState(
  { currentUserId: json.id },
  this.sendWelcomeEmail
);

Wrapping up when working with an asynchronous request make sure that your function is contained with the request (or you use a different Promise configuration). Furthermore, if your function depends on a stateful variable that could be updated make sure that your function is listed as a callback to the setState call.

« Previous Post
Modifying strong parameter values after a request
Next Post »
Building a Drawer component in React

Join the conversation

comments powered by Disqus