Development

Mastering Asynchronous State Setters in Functional Components

What is the problem?

React does some pretty incredible things, has an enormous community, and is used all over the place. My biggest complaint when learning react was this: trying to update a state property and then use that property on the following line (shown below). In this example, we have a form that requests an email address from the user and attempts to update that email address in the system permissions.

export default function EmailForm() {
  const [email, setEmail] = useState("");

  const _updateEmailPermissions = () => {
    /* do some permission updating here */
  };

  const _handleChange = (event) => {
    setEmail(event.target.value);
    _updateEmailPermissions(email);
  };

  return (
    <form>
      <label htmlFor="email">
        Email
        <input
          id="email"
          name="email"
          value={email}
          onChange={(event) => _handleChange(event)}
        />
      </label>
      <button type="submit">Submit!</button>
    </form>
  );
}

This example seems like a simple, straightforward solution, but the update will break here. When `_updateEmailPermissions` is called, our `email` value is still a blank string. The break was, at first, very unexpected to me. I reached out to a senior developer I work with, and he was able to explain more about why this happens, and the reasoning behind this. State updates in react are asynchronous, so the `_updateEmailPermissions(email)` line is being executed before the email state has had the chance to update.

Why does react do this?

React’s setState call attempts to update the state while minimizing re-renders of the component. It is best practice to use state setters instead of mutating state directly. This is because React goes through the following process each time a state setter is called. React first copies the existing state object. Then it updates the history array by pushing the copy of the state object onto the end of the array. React then updates the current state object with the new values. By doing this, react can compare the current state of data to previous states and update DOM elements that rely on -- through conditional rendering, rendering/visualizing the data -- the data that has changed. With a large state, this can create a lot of re-renders, so react does some performance optimization here. setState is asynchronous for this reason, and React will batch setState calls inside of React based event handlers to reduce component re-renders.

But I need access to my state right after. How do I do that?

Here is a simple fix if you only need the value passed to my state setter 

export default function EmailForm() {
  const [email, setEmail] = useState("");

  const _updateEmailPermissions = () => {
    /* do some permission updating here */
  };

  const _handleChange = (event) => {
    setEmail(event.target.value);
    _updateEmailPermissions(event.target.value);
  };

  return (
    <form>
       <label htmlFor="email">
        Email
         <input
          id="email"
          name="email"
          value={email}
          onChange={(event) => _handleChange(event)}
        />
      </label>
      <button type="submit">Submit!</button>
    </form>
  );
}

Using the same value that the state setter is receiving for our `_updateEmailPermissions` function works very well in this situation. Next, we will discuss a more complicated problem and an additional solution.

Using useEffect to handle state changes

Consider a slightly different example where I add the email to an array of emails and use that array to give admin permissions to users. 

export default function EmailForm({
  permissionArray,
  givePermissionsTo,
  setPermissionsArray
}) {
  const [email, setEmail] = useState("");

  useEffect(() => {
    if (permissionArray.includes(email)) {
      givePermissionsTo(email);
    }
  }, [permissionArray, givePermissionsTo, email]);

  const _handleChange = (event) => {
    event.preventDefault();
    setEmail(event.target.value);
    setPermissionsArray([...permissionArray, event.target.value]);
  };

  return (
    <form>
      <label htmlFor="email">
        Email
        <input
          id="email"
          name="email"
          value={email}
          onChange={(event) => _handleChange(event)}
        />
      </label>
      <button type="submit">Submit!</button>
    </form>
  );
}

We use the useEffect hook in this situation, which runs when components are updated. We pass the permissionsArray prop to the dependency array, showing that we want to run the useEffect hook when the `permissionsArray` prop changes. This solution works for more complex situations where you add input to an existing state data structure and then access the entire data structure or multiple properties from the updated state data structure. Some examples of this are having an object in state, updating a single property, and then accessing multiple properties from that state object immediately after. 

Dealing with Asynchronous state setters can be tricky. By using the above tips, you can navigate these challenges, and have your code execute as planned. By using the same value you pass to your state setter and using the useEffect hook, you can deal with the asynchronous nature of state setters in a predictable way. 

Newsletter
Get tips & techniques from our creative team.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
By signing up you agree to our Privacy and Policy