ReactMedium30 min

Like Dislike II

Prompt

You are starting from the solution to Like Dislike I. The core functionality works. Clicking like or dislike sends an API request and the UI updates on success.

Your task is to improve it with optimistic UI updates.

Instead of waiting for the API to respond before updating the UI, update it immediately as if the request will succeed. If the request fails, roll back to the previous state.

The mock API in this part randomly fails ~20% of the time so you can observe the rollback behavior.

Playground

Hint 1

Before updating the UI, save the current values of userReaction, likes, and dislikes into local previous variables. You will use these to restore state if the API call fails.

Hint 2

Calculate what the new state should be before making the API call. For userReaction: if the user clicked the same type that is currently active, the new reaction is null (toggle off); otherwise it is the new type. For counts: if the user is toggling off a like, decrement likes. If they are switching from like to dislike, decrement likes and increment dislikes. Apply the state updates immediately, before await toggleReaction(type).

Hint 3

In the catch block, restore all three state values using the previous variables you saved before the update. This is the rollback. The UI will snap back to what it looked like before the click.

Solution

Explanation

In Part I, the UI updates only after the API responds. With an 800ms delay, the interaction feels sluggish. Every click has visible lag. In Part II, we fix that with optimistic UI updates, a pattern used by Twitter, Reddit, YouTube, GitHub, and most modern product applications.

What is Optimistic UI?

Optimistic UI means updating the interface immediately, before waiting for the server to confirm the action. You assume the operation will succeed (which it does 99%+ of the time) and show the result right away. If it fails, you roll back to the previous state.

The name comes from optimism: you are optimistic that the server will agree with you.

Why is it a good pattern?

  • Users see an instant response with zero perceptible delay
  • It matches how physical interactions feel: you click a button and expect to see the change immediately
  • Failure rates on simple toggle requests are very low in production
  • No loading spinners needed for common interactions

Pros:

  • Dramatically better perceived performance
  • Smoother, more responsive UX, especially on slower networks
  • Widely adopted pattern in major products (GitHub stars, Twitter likes, Reddit upvotes)

Cons:

  • You must compute the expected new state client-side, which duplicates server logic
  • On failure, the UI "snaps" back, which can feel jarring if it happens visibly
  • If the server returns a different result than your prediction (due to business rules the client does not know about), you may show incorrect state briefly before correction

When not to use it:

  • Destructive or irreversible operations (deleting an account, sending a payment): users should see confirmation of actual success
  • Actions with a high or unpredictable failure rate
  • Cases where the server might apply rules that meaningfully change the result

The Pattern in Code

The pattern has four steps:

1. Save current state so you can restore it later
2. Calculate expected new state: what the UI should look like if it succeeds
3. Update UI immediately: call setters before the API call
4. On error: restore saved state (rollback)

Step 1: Save Current State Before Updating

async function handleReaction(type) {
if (isLoading) return;

// Save current state in case we need to roll back
const previousReaction = userReaction;
const previousLikes = likes;
const previousDislikes = dislikes;

// ...
}

We capture a snapshot of all three values at the start of the function. These are plain variables, not reactive. They just hold the values at the time of the click.

Step 2: Calculate the Expected New State

const newReaction = userReaction === type ? null : type;
const newLikes =
type === 'like'
? userReaction === 'like'
? likes - 1
: likes + 1
: userReaction === 'like'
? likes - 1
: likes;
const newDislikes =
type === 'dislike'
? userReaction === 'dislike'
? dislikes - 1
: dislikes + 1
: userReaction === 'dislike'
? dislikes - 1
: dislikes;

Let's trace through the logic for newLikes:

  • If the user clicked 'like':
    • Were they already liked? → decrement (likes - 1) (toggling off)
    • Were they not liked (or disliked)? → increment (likes + 1) (adding like)
  • If the user clicked 'dislike':
    • Were they previously liked? → decrement (likes - 1) (switching away from like)
    • Otherwise? → leave likes unchanged

The newDislikes calculation mirrors this symmetrically.

Common Pitfalls

This client-side calculation must exactly mirror the server's toggle logic. If they diverge, the optimistic update shows one state but the server confirmed a different state. On success you should either trust your client calculation (if you are confident it is correct) or use the server response to confirm. In production you will usually update from the server response on success to handle any business rules. In this exercise the logic is simple and client-side matches server-side exactly.

Step 3: Update the UI Immediately

// Update the UI immediately, don't wait for the API
setUserReaction(newReaction);
setLikes(newLikes);
setDislikes(newDislikes);
setIsLoading(true);

try {
await toggleReaction(type);
} catch {
// Rollback on failure
setUserReaction(previousReaction);
setLikes(previousLikes);
setDislikes(previousDislikes);
} finally {
setIsLoading(false);
}

Notice that the state updates happen before await toggleReaction(type). React will process them synchronously and re-render the UI immediately. By the time the user's eye registers the click, the button is already in its new state.

Then the API call fires in the background. The user never sees a spinner or delay.

Step 4: Rollback on Failure

In the catch block, we restore all three values to what they were before the click:

catch {
setUserReaction(previousReaction);
setLikes(previousLikes);
setDislikes(previousDislikes);
}

The mock API in Part II randomly fails 20% of the time. Try clicking several times. You will occasionally see the button flash active for a moment, then snap back when the rollback fires.

isLoading in Optimistic UI

We still keep setIsLoading(true) even in the optimistic version. Even though the UI has already updated, we disable both buttons while the request is in flight. This prevents a second click before the first resolves, which could fire a second API call on top of the first and produce unpredictable results when one of them fails and rolls back. isLoading still prevents those race conditions.

Communicate the Pattern in Interviews

When you start implementing this, say it out loud: "I'm going to update the UI optimistically. I'll save the previous state first, apply the new state immediately, and restore the previous state in the catch block if the request fails." This tells the interviewer you know the pattern and you are deliberately applying it, not just writing code that happens to work.

Also mention when you would and would not use this pattern. Saying "I would not do this for a destructive operation like deleting an account" shows you understand the tradeoffs, not just the implementation.

Interviewer Criteria

JavaScript

  • Are previous state values saved before any updates, not after?

  • Is the new state calculated correctly for all four transitions: like→none, none→like, dislike→none, none→dislike, and the cross-reaction switches?

  • Are state setters called before await and not after?

  • Is rollback in the catch block, restoring all three values?

React

  • Is isLoading still used to disable buttons, preventing race conditions even with optimistic updates?

  • Does the UI immediately reflect the click without any visible delay?

  • Does the UI snap back correctly on failure?

Conceptual Understanding

  • Can the candidate explain what optimistic UI means and why it is useful?

  • Can they name real examples where this pattern is used (YouTube likes, GitHub stars, Reddit votes)?

  • Can they articulate the tradeoffs: when to use it and when not to?

  • Do they mention that destructive actions should NOT be optimistic?

  • Can they explain why client-side state calculation must mirror server logic?

Time Checkpoints

  • 10:00 AM

    Interview starts 👥

  • 10:03 AM

    Interviewer asks: how would you make the like button feel instant?

  • 10:05 AM

    Candidate explains the optimistic UI pattern before coding

  • 10:10 AM

    Previous state variables saved at the top of handleReaction

  • 10:15 AM

    New state calculated client-side for all transition cases

  • 10:20 AM

    State setters called before await, UI updates immediately on click

  • 10:30 AM

    Rollback implemented in catch block, tested with the 20% failure rate

  • 10:40 AM

    Discussion: when to use optimistic UI vs when not to

  • 10:55 AM

    Discussion with interviewer about production considerations (server response verification)

  • 11:00 AM

    Interview ends ✅