Erik Aybar

Optimistic UI Updates in React using component state

April 30, 2019

Let’s walk through a step-by-step progression of developing an items list UI with React that enables deleting each of the items by clicking a button that submits an *HTTP request.

We’ll begin with a static UI and first introduce the concept of “loading”. Then we’ll handle failed requests before refactoring towards a more polished feel using optimistic UI updates.

To contrast the difference in feel between the typical loading UI and optimistic update UI let’s see the two side-by-side:

Side-by-side comparison of a “loading” UI vs. an “optimistic update” UI

The progression we’ll be following is:

  1. Build static UI, hardcoding component state
  2. Update state asynchronously using Promise and setState()
  3. Add a “loading” state to indicate request in progress
  4. Account for failed requests and conditionally display errors
  5. Optimistic UI update in React using setState() and closures
  6. Granularly Restore State for Improved Optimistic UI Updates

*This post is part of 2 of 2. See part 1 here Optimistic UI Updates in React (part 01).

The examples provided and videos linked to are from the second half of my Optimistic UI Updates in React course on egghead.io. Head on over and check out the course overview/preview (free) to get a feel for what the course is all about.

egghead.io course: Optimistic UI Updates with React

1) Build static UI, hardcoding component state

We’ll begin with a fresh React component (see docs: React.Component) and define the data structure that we’ll be working with. We can hardcode this for now right in render as items. This will be a simple array of objects, each with an “id” and “title” property. Then, we’ll render these as a list of items and add a button with a click handler for each of them (this button will not do anything quite yet).

Next, we will promote our hard-coded items to Component state as state.items so that we can manipulate and maintain it based on user interactions.

Example: Rendering static UI and hardcoding component state

class ItemsList extends React.Component {
  state = {
    items: [{id: 1, title: 'An item' /* ... */}],  }

  render() {
    const {items} = this.state
    return (
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            <button>Delete</button>
          </li>
        ))}
      </ul>
    )
  }
}

2) Update state asynchronously using Promise and setState()

First, we’ll hook up each of the items’ buttons to delete the item on click. This will fire off a *request to an API which will return a Promise. Making use of Promise.prototype.then we’ll update our component state after the request is complete. Since we need to update state based on current state, we’ll use setState() with an updater function and remove the item from the list with Array.prototype.filter.

🎬 Update state asynchronously in React using Promise and setState() (2:25)

Example: Hooking up a React component to communicate with an API and update items upon request success using setState()

class ItemsList extends React.Component {
  state = {
    items: [],
  }

  deleteItem(id) {
    // 1) Submit HTTP request (returns a Promise)
    deleteItemRequest(id).then(() => {
      // 2) Update items upon request success      this.setState(state => ({        items: state.items.filter(item => item.id !== id),      }))    })
  }

  render() {
    const {items} = this.state
    return (
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            <button onClick={() => this.deleteItem(item.id)}>Delete</button>
          </li>
        ))}
      </ul>
    )
  }
}

3) Add a “loading” state to indicate request in progress

Rather than leaving our users hanging wondering if their action of clicking the button had any effect, we can indicate visually whether we are “loading” to the user. We will “begin loading” before submitting our request to our *API and then “end loading” once the request is complete using Promise.prototype.then and setState().

🎬 Add “loading” to UI for request in progress using setState() (1:02)

Example: Adding a loading state to keep track of and indicate whether a request is in progress

class ItemsList extends React.Component {
  state = {
    items: [],
    loading: false,  }

  deleteItem(id) {
    // 1) Immediately "begin loading"
    this.setState({loading: true})    // 2) Submit HTTP request (returns a Promise)
    deleteItemRequest(id).then(() => {
      // 3) Success: Update items and "end loading"
      this.setState(state => ({
        items: state.items.filter(item => item.id !== id),
        loading: false,      }))
    })
  }

  render() {
    const {items, loading} = this.state
    // This could be a dynamic className or CSS-in-JS based styles    const loadingStyles = {      opacity: loading ? 0.6 : 1,    }    return (
      <ul styles={loadingStyles}>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            <button onClick={() => this.deleteItem(item.id)}>Delete</button>
          </li>
        ))}
      </ul>
    )
  }
}

4) Account for failed requests and conditionally display errors

So far we’ve yet to account for any errors encountered during a request. We can add a catch() handler (Promise.prototype.catch) to our request’s promise where we will add an error message to component state. We will display the message to our users once an error has occurred using conditional rendering.

🎬 Handle rejected promise and display error to users (1:49)

Example: Handle failed requests in React using setState(). Conditional rendering to display error to user.

class ItemsList extends React.Component {
  state = {
    items: [],
    loading: false,
    errorMessage: null,  }

  deleteItem(id) {
    // 1) Immediate "begin loading"
    this.setState({loading: true})
    // 2) Submit HTTP request (returns a Promise)
    deleteItemRequest(id)
      // 3a) Success: Update items and "end loading"
      .then(() => {
        this.setState(state => ({
          items: state.items.filter(item => item.id !== id),
          loading: false,
        }))
      })
      // 3b) Failure: Store error message.      .catch(error => {        this.setState(state => ({          errorMessage: 'Whoops! Something went wrong. Please try again.',        }))      })
  }

  render() {
    const {items, loading, errorMessage} = this.state
    // ...
    return (
      <div>
        {/* ... */}
        {errorMessage && <p className="error-text">{errorMessage}</p>}      </div>
    )
  }
}

5) Optimistic UI update in React using setState() and closures

Lastly, we can refactor our UI away from a more typical loading experience to an optimistic UI update approach to give our users a faster, more snappy experience.

Instead of loading while our request is in progress, we can assume request success, immediately update the UI, and account for displaying an error and reverting state in the event of a failure. Thanks to the simplicity and power of setState() combined with making use of Javascript’s lexical scoping and closures, we can accomplish this relatively *easily in React.

*This is a naive solution. Read further to explore a more robust approach. While naive, a simple solution like this could very well meet your needs for many use cases while avoiding additional complexity.

🎬 Optimistic UI update in React using setState() (3:47)

Example: Optimistic UI update in React with setState()

class ItemsList extends React.Component {
  state = {
    items: [],
    errorMessage: null,
  }

  deleteItem(id) {
    // 1) Snapshot relevent current state to restore    const stateToRestore = {
      items: this.state.items,
    }

    // 2) Assume request success. Immediately update state    this.setState(state => ({
      items: state.items.filter(item => item.id !== id),
    }))

    deleteItemRequest(id)
      // 3) Restore relevant state upon failure      .catch(error => {
        this.setState(state => ({
          errorMessage: 'Whoops! Something went wrong. Please try again.',
          ...stateToRestore,
        }))
      })
  }

  render() {
    // ...
  }
}

6) Granularly Restore State for Improved Optimistic UI Updates

Our initial, simplified approach may be fine for a number of scenarios, but is not the most robust solution. We’ve left the potential for inconsistent state due to the fact that between taking the snapshot of current state (stateToRestore) and restoring it after a failure, the user could have performed additional actions. Consider this sequence in events that will result in a bug with our previous solution:

  1. User deletes “Item 3” (snapshot taken at this time, “Item 3” removed immediately). Request pending…
  2. User deletes “Item 2” (“Item 2” removed immediately). Request pending…
  3. The request for “Item 3” fails and we revert state using the snapshot taken before the user deleted “Item 2”.
  4. The request for “Item 2” succeeds.

The bug: Even though “Item 2” has truly been deleted from our *database it has been visually restored to the screen along with “Item 3” due to the snapshot we took in step 1 when the user clicked delete for “Item 3”.

One solution: Instead of taking a snapshot of our entire state, we can take a snapshot of the target item and use that upon failure to more precisely restore the individual item. This eliminates the possibility of erroneously restoring deleted items and giving our users the false impression of failure.

🎬 Restore Target Items in React State for Improved Optimistic UI Updates (2:57)

Example: Restore the item which failed individually to avoid incorrectly restoring deleted items.

class ItemsList extends React.Component {
  state = {
    items: [],
    errorMessage: null,
  }

  deleteItem(id) {
    // 1) Snapshot target item to restore in the case of failure    const {items} = this.state
    const targetIndex = items.findIndex(item => item.id === id)
    const targetItem = items[targetIndex]

    // 2) Assume request success. Immediately update state
    this.setState(state => ({
      items: state.items.filter(item => item.id !== id),
    }))

    deleteItemRequest(id)
      // 3) Restore relevant state upon failure
      .catch(error => {
        this.setState(state => ({
          errorMessage: 'Whoops! Something went wrong. Please try again.',
          // Put the item back where it was originally          items: [
            ...state.items.slice(0, targetIndex),
            targetItem,
            ...state.items.slice(targetIndex),
          ],
        }))
      })
  }

  render() {
    // ...
  }
}

What about hooks? 🎣

Yes, I know… This could be simplified with hooks https://reactjs.org/docs/hooks-reference.html 😅

This course was released last year, long before hooks in React came to life. I do plan on revisiting the course and updating to highlight how to solve with hooks instead of class components. Overall, the concept is the same except for shuffling around and simplifying a bit. If you’re feeling adventurous take a crack at porting over one of the examples and ping me with it @erikaybar_!


If you are looking to further sink your teeth into more extensive and more advanced React video content, I must recommend the very well put together egghead.io React courses from Kent C. Dodds:



Erik Aybar

👋🏽 Hi! I'm Erik Aybar. I'm a software person working remotely from St. George, Utah. This is my blog.