React useOptimistic
Use optimistic state only when the interface can clearly distinguish a pending idea from a confirmed result.
How it works
Optimistic UI shows a proposed change before its asynchronous operation finishes. It must also handle rejection. useOptimistic can layer a temporary value over confirmed state while an action runs. This lab uses a delayed local acceptance rule: the name fail is rejected deliberately. No network service or permanent save is involved. The pending row is labelled, and failure removes it instead of leaving an unearned success message.
Run the code
Use a separate empty lab folder with Node 22.12+ in the 22 series. Save the files below using their displayed paths; browsers may download only the basename, so create src, public or tests folders as listed. The pinned project uses React and React DOM 19.3.1 with Vite 7.3.1. Run npm install, then npm run dev and open the local address Vite prints. React examples do not run in Bhylu’s current playground.
No Apache, MySQL or change to Bhylu’s server configuration is needed. These are local teaching projects, not a publishing package. Keep generated dependencies and lockfiles in your own lab. Review the project setup.
Example
import React, {
StrictMode,
useState,
useRef,
useActionState,
useOptimistic,
} from 'react';
import { createRoot } from 'react-dom/client';
import './style.css';
function App() {
const [items, setItems] = useState([]);
const nextId = useRef(1);
const [visible, addPending] = useOptimistic(items, (current, item) => [
...current,
item,
]);
const [message, action, pending] = useActionState(async (previous, data) => {
const name = String(data.get('item') || '').trim();
if (!name) return 'Enter an item';
addPending({ id: 'pending', name: name + ' (pending)' });
await new Promise((resolve) => setTimeout(resolve, 100));
if (name.toLowerCase() === 'fail') return 'Local rejection: item was not added';
const item = { id: String(nextId.current++), name };
setItems((rows) => [...rows, item]);
return 'Accepted in this page only';
}, 'No additions');
return (
<main>
<h1>Neighbourhood lending desk</h1>
<form action={action}>
<label>
Item label
<input name="item" required />
</label>
<button disabled={pending}>Try local addition</button>
</form>
<output aria-live="polite">{message}</output>
<ul>
{visible.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</main>
);
}
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
);
Expected result
Basket is retained after local acceptance. Trying fail shows a rejection and leaves only the earlier accepted row.
The optimistic layer is temporary; the confirmed array changes only after acceptance. The disabled submit button prevents concurrent additions in this deliberately small example. A real system needs unique request IDs, duplicate handling, reconciliation and a reliable server response. The artificial delay tests the pending and rollback paths without pretending that an item was stored remotely.
For API contracts and version differences, use React’s official documentation.