← Back to React Course | Chapter 10: Data Fetching & Async | Lesson 10 of 10

Optimistic UI Updates

An optimistic update is like telling your friend 'sure, I'll be there!' before actually checking your calendar — you assume yes and only correct yourself if it turns out to be a conflict.

Updating the UI Before the Server Confirms

In a normal flow, you'd wait for a server response before updating the UI, which can feel sluggish for small, usually-successful actions. An optimistic update flips this: change the local state immediately, assuming success, then quietly fire off the real request in the background.

Note: Optimistic updates work best for actions that rarely fail, like liking a post or toggling a checkbox — the risk of a visible revert is low.

Warning: For actions with a meaningful chance of failure (like a payment), the sudden visual revert on failure can be more jarring than just waiting for confirmation upfront.

Example: Updating the UI Before the Server Confirms

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
function App() {
  const [liked, setLiked] = React.useState(false);
  const handleLike = () => {
    setLiked(true); // update immediately
    // fetch('/api/like', { method: 'POST' }); // fires in background
  };
  return <button onClick={handleLike}>{liked ? "Liked!" : "Like"}</button>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

Reverting on Failure

Since the optimistic update assumes success, you need a plan for when the real request fails: storing the previous value before changing it lets you roll the UI back to that exact prior state if the server responds with an error.

Note: Capture the previous value in a local variable right before applying the optimistic change, so it's available in the catch block.

Warning: Skipping the revert step means a failed request leaves the UI silently lying about the true server state.

Example: Reverting on Failure

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
function App() {
  const [liked, setLiked] = React.useState(false);
  const handleLike = () => {
    const previous = liked;
    setLiked(true);
    const requestFailed = true; // simulate a failed request
    if (requestFailed) setLiked(previous);
  };
  return <button onClick={handleLike}>{liked ? "Liked!" : "Like (will revert)"}</button>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

Optimistic Updates for Lists

The same idea extends to adding or removing items from a list: add the new item to local state immediately, send the real request in the background, and remove it again (or show an error) if the request ultimately fails.

Note: Give optimistically-added items a temporary local ID, replacing it with the real server-assigned ID once the request succeeds.

Warning: Removing the wrong item during a revert (if IDs aren't tracked carefully) can silently delete a different, unrelated item from the list.

Example: Optimistic Updates for Lists

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
function App() {
  const [items, setItems] = React.useState(["First item"]);
  const addItem = () => {
    setItems(prev => [...prev, "New item (optimistic)"]);
    // fetch('/api/items', { method: 'POST' }).catch(() => {
    //   setItems(prev => prev.slice(0, -1)); // revert on failure
    // });
  };
  return <div><button onClick={addItem}>Add Item</button><ul>{items.map((i, idx) => <li key={idx}>{i}</li>)}</ul></div>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>
Common Mistakes
  1. Not reverting the optimistic change if the actual server request fails, leaving the UI permanently showing incorrect data.
  2. Applying the optimistic update after the request instead of before, defeating the whole point of feeling instant.
  3. Not giving any visual cue that an update is pending confirmation, if that matters for the use case.
Chapter Summary
  • An optimistic update changes the UI immediately, assuming a request will succeed, before waiting for the server's response.
  • This makes actions like liking a post or checking off a to-do feel instant rather than sluggish.
  • If the actual request fails, the UI must be rolled back to its previous state.
  • Storing the previous value before updating makes reverting straightforward if needed.
Browser Support

No React-version restriction — a UI pattern built from useState and standard request handling.

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.