Optimistic writes without the choreography
Click “like” and the count goes up instantly. The request runs in the background. If it fails, the count snaps back and you get a toast. On success, the real count refetches. The button shows a pending state throughout.
This is the modern write: optimistic, reversible, with a pending flag. React 19
gives you the primitives — useActionState, useOptimistic, useTransition —
but wiring them together correctly is its own small project.
React — what you actually write
Section titled “React — what you actually write”import { useState, useOptimistic, useTransition } from "react"
export function LikeButton({ id, initial }: { id: string; initial: number }) { const [likes, setLikes] = useState(initial) const [optimisticLikes, applyOptimistic] = useOptimistic(likes) const [isPending, startTransition] = useTransition()
const like = () => { startTransition(async () => { applyOptimistic(likes + 1) // paint instantly try { const next = await api.like(id) setLikes(next) // real commit refetchLikeCount() // invalidate the source of truth } catch (e) { toast(String(e)) // useOptimistic auto-reverts; handle the rest } }) }
return <button onClick={like} disabled={isPending}>♥ {optimisticLikes}</button>}Three hooks, a shadow pair (likes + optimisticLikes), and a precise
ordering — optimistic-inside-the-transition, commit-after-await, revert-in-catch.
Get the order wrong and the count flickers, double-counts, or never reverts.
Reactra — what you describe
Section titled “Reactra — what you describe”export component LikeButton { param id: string state likes: number = 0 resource likeCount(id) => api.likeCount(id)
command like() => api.like(id) optimistic { likes = likes + 1 } // paint instantly, auto-revert on throw invalidate [likeCount] // refetch the truth on success rollback(e) { toast(e) } // failure side effects (no re-throw)
view { <button onClick={like} disabled={like.pending}>♥ {likes}</button> }}import { useEffect, useOptimistic, useState, useTransition } from "react"import { __getResourceCache, useResource } from "@reactra/resource"import { useRoute } from "@reactra/router"
export const LikeButton = () => { const __route = useRoute() const id = __route.params.id const [__base_likes, setLikes] = useState((0) as number) const [likes, __setOpt_likes] = useOptimistic(__base_likes) const likeCount = useResource(id, (id) => (api.likeCount(id)), { resourceName: "likeCount" }) const [__pending_like, __start_like] = useTransition(); const like = (__payload?: unknown) => __start_like(async () => { __setOpt_likes(likes + 1); try { await (async () => (api.like(id)))(__payload); setLikes(likes + 1); __getResourceCache().invalidate(["likeCount"]); } catch (__e) { ((e) => { toast(e) })(__e); } }); like.pending = __pending_like useEffect(() => { const h = globalThis.__REACTRA_TEST__; if (h) h.update("LikeButton", { likes, likeCount }) }) return (<> <button onClick={like} disabled={like.pending}>♥ {likes}</button> </>)}export default LikeButtonThe optimistic update is one plain assignment. The shadow pair, the transition, the commit ordering, the revert, the refetch — generated.
What disappeared
Section titled “What disappeared”- The shadow pair (
likes+optimisticLikes) → you writelikes; the compiler keeps the mirror useTransition+ the “optimistic must be inside the transition” rule → thecommandform- The ordering (apply → await → commit → revert) you hand-sequence → generated, the same way every time
- Manual
refetch/invalidate plumbing →invalidate [likeCount] - The pending flag wiring →
like.pending
Same interaction. None of the choreography.
It’s not a new runtime
Section titled “It’s not a new runtime”command is the write-side counterpart to resource, and like resource it
compiles to React’s own primitives: the block form (command f() { … }) to
useActionState; the arrow form to useTransition, with optimistic →
useOptimistic, invalidate → resource cache eviction, rollback → the failure
handler. A plain command with no observability adds zero runtime — it’s the
hooks you’d have written.
You write the write. The compiler writes the part where the order matters.