Consider this Next.js component that updates a list of items optimistically when a button is clicked.
import { useState } from 'react';
export default function ItemList() {
const [items, setItems] = useState(['apple', 'banana']);
async function addItem() {
setItems([...items, 'orange']); // optimistic update
await fetch('/api/add', { method: 'POST', body: JSON.stringify({ item: 'orange' }) });
}
return (
<div>
<ul>
{items.map(item => <li key={item}>{item}</li>)}
</ul>
<button onClick={addItem}>Add Orange</button>
</div>
);
}import { useState } from 'react'; export default function ItemList() { const [items, setItems] = useState(['apple', 'banana']); async function addItem() { setItems([...items, 'orange']); // optimistic update await fetch('/api/add', { method: 'POST', body: JSON.stringify({ item: 'orange' }) }); } return ( <div> <ul> {items.map(item => <li key={item}>{item}</li>)} </ul> <button onClick={addItem}>Add Orange</button> </div> ); }
Think about what 'optimistic update' means: updating UI before server confirms.
Optimistic updates immediately change the UI assuming the server call will succeed. Here, 'orange' is added to the list right away.
In this Next.js component, the optimistic update adds 'grape' to the list, but the server call fails.
import { useState } from 'react';
export default function FruitList() {
const [items, setItems] = useState(['apple', 'banana']);
async function addGrape() {
setItems([...items, 'grape']); // optimistic update
try {
const res = await fetch('/api/add-grape', { method: 'POST' });
if (!res.ok) throw new Error('Failed');
} catch {
setItems(items); // rollback
}
}
return (
<div>
<ul>
{items.map(item => <li key={item}>{item}</li>)}
</ul>
<button onClick={addGrape}>Add Grape</button>
</div>
);
}import { useState } from 'react'; export default function FruitList() { const [items, setItems] = useState(['apple', 'banana']); async function addGrape() { setItems([...items, 'grape']); // optimistic update try { const res = await fetch('/api/add-grape', { method: 'POST' }); if (!res.ok) throw new Error('Failed'); } catch { setItems(items); // rollback } } return ( <div> <ul> {items.map(item => <li key={item}>{item}</li>)} </ul> <button onClick={addGrape}>Add Grape</button> </div> ); }
Rollback means undoing the optimistic change if the server call fails.
When the server call fails, the code resets the state to the original 'items' array, removing 'grape'.
Choose the code snippet that correctly updates state optimistically and rolls back on error.
Remember to save the previous state before updating optimistically.
Option A saves the previous state, updates optimistically, and rolls back if the fetch fails. Option A updates state inside try before await, which can cause issues if fetch throws before state update.
Review this code snippet:
const [items, setItems] = useState(['apple']);
async function addItem() {
setItems([...items, 'mango']);
try {
await fetch('/api/add-mango', { method: 'POST' });
} catch {
setItems(items);
}
}Why might the rollback fail to restore the original list?
const [items, setItems] = useState(['apple']); async function addItem() { setItems([...items, 'mango']); try { await fetch('/api/add-mango', { method: 'POST' }); } catch { setItems(items); } }
Consider the conditions under which fetch() rejects its promise.
fetch() does not reject on HTTP error status codes (4xx/5xx); it only rejects on network errors (e.g., no connection). On server errors like 500, the promise resolves, res.ok is false, but no throw occurs, so catch is skipped and no rollback happens.
Choose the best explanation for why developers use optimistic updates in Next.js applications.
Think about how users feel when UI updates instantly.
Optimistic updates improve user experience by making the UI feel faster and more responsive, showing changes immediately while the server processes the request.