Pick a depth. Each prompt opens in your AI pre-loaded with the lesson. Click a row to preview the prompt.
Optimistic UI encourages retrying failed mutations because the UI already shows the desired state — the user just wants the server to agree. But retrying a non-idempotent mutation is dangerous: incrementing a counter twice because a network timeout made you think the first request failed means your data is wrong. The solution is idempotent API design: either use PUT (set the value absolutely) instead of increment operations, or attach a client-generated idempotency key to POST requests so the server recognizes and deduplicates retries. Every mutation that will be paired with optimistic UI should be designed to be safely retried, because network timeouts and flaky connections will happen in production.
Two implementations of the same 'set view count' feature: a non-idempotent POST that adds 1 each time it is called, and an idempotent PUT that sets the value absolutely.
NonIdempotentCounter, set the failure rate to 1.0 and add a retry in the catch block — add console.log('attempt') to verify two API calls fire per click, showing the double-increment hazard.IdempotentCounter, add automatic retry logic in the catch block (up to 3 times with 200ms delays) — verify it is safe because it always sends the same absolute newCount value.crypto.randomUUID()) inside handleClick and log it for both the original attempt and any retries — verify the same key is used each time.idempotentSetCount to log the count argument it receives — click once with a failure configured, then manually retry, and verify both calls log the identical target count.import { useState } from "react";
// NON-IDEMPOTENT: calling this twice increments twice
async function nonIdempotentIncrement(postId: string): Promise<void> {
await new Promise((r) => setTimeout(r, 400));
if (Math.random() < 0.3) throw new Error("timeout");
// server: UPDATE posts SET views = views + 1 WHERE id = ?
}
// IDEMPOTENT: calling this with the same value sets it absolutely
async function idempotentSetCount(postId: string, count: number): Promise<void> {
await new Promise((r) => setTimeout(r, 400));
if (Math.random() < 0.3) throw new Error("timeout");
// server: UPDATE posts SET views = ? WHERE id = ?
}
export function NonIdempotentCounter({ postId }: { postId: string }) {
const [count, setCount] = useState(100);
async function handleClick() {
const prev = count;
setCount((c) => c + 1); // optimistic
try {
await nonIdempotentIncrement(postId);
} catch {
setCount(prev); // rollback; but if we retry, server increments again
console.warn("Non-idempotent: retrying would double-increment");
}
}
return <button onClick={handleClick}>Views: {count}</button>;
}
export function IdempotentCounter({ postId }: { postId: string }) {
const [count, setCount] = useState(100);
async function handleClick() {
const newCount = count + 1;
const prev = count;
setCount(newCount); // optimistic
try {
await idempotentSetCount(postId, newCount);
} catch {
setCount(prev); // rollback; safe to retry with same newCount
}
}
return <button onClick={handleClick}>Views: {count}</button>;
}