React 19 organized "the seam between async and UI" almost like a language feature. Used correctly, it greatly reduces the boilerplate of loading branches and rollbacks.
1. use(promise): read async during render
Conventionally you wrote the boilerplate of "fetch in useEffect → store in useState." use reads a Promise directly during render and Suspends until it resolves. You can externalize loading-state management to <Suspense>.
"use client";
import { use } from "react";
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
// 解決まで Suspend → 親の <Suspense fallback> が表示される
const comments = use(commentsPromise);
return (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.text}</li>
))}
</ul>
);
}
Stream by combining with a Server Component
The royal road for use is the pattern "start the fetch in a Server Component (don't await) → pass the Promise to a Client Component → use it on the client." The server starts flushing HTML without waiting, and it streams as soon as the data resolves.
// page.tsx(Server Component)
import { Suspense } from "react";
export default function Page() {
// ❗ await しない。Promise のまま子へ渡す
const commentsPromise = fetchComments();
return (
<Suspense fallback={<CommentsSkeleton />}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);
}
This achieves both "fast initial display (LCP)" and "insertion after the data arrives" (see Core Web Vitals optimization).
Context can also be read conditionally
Because use can be called even after an early return, it enables conditional reads that useContext couldn't express.
function Heading({ children }: { children?: React.ReactNode }) {
if (children == null) return null; // 早期 return
const theme = use(ThemeContext); // ✅ return の後でも OK(useContext は不可)
return <h1 className={theme.heading}>{children}</h1>;
}
2. use's fatal pitfall: don't create the Promise inside render
This is the point of most accidents. If you create a Promise inside a Client Component's render, a new Promise is made on every re-render, and it never resolves (infinite suspend).
// ❌ 絶対にダメ:レンダーのたびに新しい Promise → 無限サスペンド
function Bad() {
const data = use(fetch("/api/data").then((r) => r.json()));
return <div>{data.title}</div>;
}
The correct answer is to put the Promise's origin outside render.
- Create it in a Server Component and pass via props (the pattern from the previous chapter; most recommended).
- Return the same Promise from a cache layer (module scope or a dedicated cache).
- Delegate to an event handler or a data library (TanStack Query, etc.).
If you also need refetch, cache, and invalidation, a dedicated library like TanStack Query fits better than using use directly. use is a low-level primitive API and holds no cache strategy. Using the right tool for the job is KISS.
3. useOptimistic: instant feedback and automatic rollback
Updating the UI first without waiting for the network response dramatically improves perceived speed. useOptimistic realizes this "optimistic update" without hand-writing the rollback.
const [optimisticState, addOptimistic] = useOptimistic(actualState, updateFn);
actualState: the real value when there's no pending operation.updateFn(currentState, optimisticValue): a function that computes the optimistic state.addOptimistic(value): fire an optimistic update (call inside an action/transition).
Crucial property: when the action completes, optimisticState automatically converges to actualState. On success the real value is updated; on failure the real value hasn't changed, so the optimistic entry naturally disappears. No manual rollback is needed.
Example of chat sending combined with a Server Action
"use client";
import { useOptimistic } from "react";
type Message = { text: string; sending?: boolean };
function Thread({
messages,
sendMessage,
}: {
messages: Message[];
sendMessage: (text: string) => Promise<void>; // Server Action
}) {
const [optimistic, addOptimistic] = useOptimistic(
messages,
(state, newText: string): Message[] => [...state, { text: newText, sending: true }],
);
async function formAction(formData: FormData) {
const text = String(formData.get("message") ?? "");
if (!text) return;
addOptimistic(text); // ① 即座に「送信中」で表示
await sendMessage(text); // ② 完了後、実体が更新され optimistic は収束
}
return (
<form action={formAction}>
<ul aria-live="polite">
{optimistic.map((m, i) => (
<li key={i}>
{m.text}
{/* 送信中であることを支援技術にも伝える */}
{m.sending && <small role="status"> 送信中…</small>}
</li>
))}
</ul>
<label htmlFor="msg">メッセージ</label>
<input id="msg" name="message" />
</form>
);
}
addOptimistic must be called inside an action (via <form action> or useTransition). For Server Action details, see the Next.js 16 Server Actions practical guide.
4. Accessibility: make optimistic updates "audible"
Instant updates are kind visually, but you also need to convey the state change to screen-reader users.
- Wrap dynamically growing regions in
aria-live="polite"so additions and updates are read aloud. - Notify "sending" / "failed" with
role="status"/role="alert". - Set the in-flight control to
aria-busy={true}so the look (semi-transparent) matches the meaning.
Don't make the "speed" of optimistic updates the property of sighted users only. That is accessible UX (see the WCAG 2.2 implementation guide).
5. Security: don't make optimistic state the "truth"
What useOptimistic shows is purely the client's prediction.
- Don't use optimistic state for permission decisions. Treating "it showed success optimistically, so it's done" is dangerous. The server is the single source of truth.
- Always prepare feedback on failure. If the optimistic entry merely disappears automatically, the user is left wondering "did it send?" Catch errors on the action side and surface a re-send path or message.
- Guarantee idempotency. Deduplicate on the server side against double-clicks and re-sends (Server Actions idempotency).
6. useOptimistic vs. TanStack Query — which to use?
| Aspect | useOptimistic | TanStack Query's onMutate |
|---|---|---|
| Scope | per action/form | app-wide cache |
| Rollback | automatic (converges to real value) | manual (snapshot stash) |
| Reflection range | that component | multiple screens / multiple components |
| Refetch/cache | none | yes (invalidate/refetch) |
| Best-suited scene | instant reflection of a single form | cross-cutting server-state management |
For showing the in-flight state of a single form, useOptimistic is the lowest cost. If you want to propagate the same update across multiple screens or need a cache strategy, TanStack Query fits (details on choosing).
7. Other React 19 updates (quick reference)
| API | Use | Related article |
|---|---|---|
useActionState | handle a form-submit result state type-safely | Server Actions |
useFormStatus | get pending (sending) on the button side | same |
useTransition | mark heavy updates as "non-urgent" and protect responsiveness | Core Web Vitals |
useDeferredValue | separate instant input reflection from heavy recomputation | same |
| ref as prop | pass ref directly as a prop without forwardRef | — |
These complement each other. Centered on use / useOptimistic, assemble with a division of roles: useActionState for forms, useTransition for responsiveness.
8. Testing: protect by behavior, not by feel
- E2E excels at verifying optimistic updates. Assert with Playwright the flow where "sending" appears right after submit and changes to the confirmed display after completion (Playwright E2E design).
- Rollback on failure too: mock the API to return 500 and confirm the optimistic entry disappears and an error shows.
use's suspend: verify with E2E that the<Suspense>fallback is shown and is then replaced by the content.
9. Anti-patterns
- ❌ Creating a Promise inside render and passing it to
use. The biggest cause of infinite suspend. Create outside render. - ❌ Replacing all
useEffect+useStatefetches withusewithout a cache. Use a data library if you need refetch and dedup. - ❌ Using optimistic state for permission or final decisions. The server is the truth. Keep it to UI prediction.
- ❌ Omitting feedback on failure. Merely disappearing automatically is unkind. Surface an error and a re-send path.
- ❌ Not adding
aria-live/role=status. Speed becomes the property of sighted users only. - ❌ Calling
addOptimisticoutside an action. Use it in a transition/action context.
10. FAQ (frequently asked questions)
Q. Does use completely replace data fetching in useEffect?
A. It can replace the fetch itself, but it holds no cache, refetch, or invalidation. If those are needed, combine it with a data library (TanStack Query, etc.).
Q. Why can use be called in a conditional branch even though it's a hook?
A. use has a different internal implementation from other hooks and is designed so it can be called conditionally or after an early return. Still, it can only be used inside a component/hook.
Q. On useOptimistic failure, do I write the rollback myself?
A. Not needed. When the action completes, optimisticState automatically converges to actualState. On failure the real value doesn't change, so the optimistic entry disappears. Just prepare an error display.
Q. use(promise) gives infinite loading.
A. Almost certainly the cause is "creating the Promise inside render." Create it in a Server Component or a cache layer and pass the same Promise.
Q. Are these Next.js-only?
A. No, they are React 19 features. However, use's streaming shines most when combined with a framework that has Server Components (Next.js, etc.).
Conclusion: treat async and optimism as "language features"
React 19's use and useOptimistic lifted "loading branches" and "optimistic-update rollback" — which we used to write as boilerplate — into framework primitives.
- Read async during render with
use(promise)and entrust the state to<Suspense>. - Create the Promise outside render (Server Component / cache) to avoid infinite suspend.
- Show instant feedback with
useOptimisticand let it auto-converge on completion. - Always attach a11y (aria-live / role=status) and security (server is the truth, idempotency).
- Right tool for the job. TanStack Query for cross-cutting cache,
useOptimisticfor a single form.
Perceived speed is directly tied to trust. Used correctly, the new APIs let you build fast, safe, and universally-reachable UX with less code.
If you need design and implementation for an app where real-time and instant responsiveness create value, feel free to reach out. The case study below introduces the process of designing and implementing a responsiveness-focused real-time app that doesn't break even when many people edit simultaneously.