# A thorough guide to React 19's new hooks [2026 edition] — mastering use and useOptimistic at production quality

> A practical guide to mastering React 19's new APIs, use and useOptimistic, at production quality. With real code it covers: in-render async reads with use(promise) and Suspense streaming, the infinite-suspend pitfall, instant feedback and automatic rollback with useOptimistic, and accessibility, security, and testing.

- Published: 2026-06-24
- Author: 友田 陽大
- Tags: React, Next.js, フロントエンド, TypeScript, パフォーマンス, アクセシビリティ
- URL: https://tomodahinata.com/en/blog/react-19-use-useoptimistic-hooks-practical-guide
- Category: Frontend
- Pillar guide: https://tomodahinata.com/en/blog/nextjs-16-app-router-cache-components-data-fetching

## Key points

- use(promise) is a new API for reading an async value during render; pass it a Promise and it Suspends, surfacing the nearest Suspense.
- The biggest pitfall is creating the Promise inside render. Create it in a Server Component or a cache layer and pass it in to avoid infinite suspend.
- useOptimistic shows instant UI while sending and automatically converges to the real value when the action completes, so you don't hand-write rollback.
- Optimistic state is only the client's prediction. Don't use it for permissions or final decisions; treat the server as the single source of truth and guarantee idempotency.
- Use the right tool: TanStack Query for cross-cutting cache, useOptimistic for a single form's instant reflection.

---

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>`.

```tsx
"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.

```tsx
// 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](/blog/core-web-vitals-nextjs-inp-lcp-cls-optimization-guide)).

### 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.

```tsx
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).**

```tsx
// ❌ 絶対にダメ：レンダーのたびに新しい 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](/blog/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.**

```tsx
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

```tsx
"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](/blog/nextjs-16-app-router-cache-components-data-fetching).

---

## 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](/blog/react-nextjs-web-accessibility-wcag22-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](/blog/nextjs-16-app-router-cache-components-data-fetching)).

---

## 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](/blog/tanstack-query)).

---

## 7. Other React 19 updates (quick reference)

| API | Use | Related article |
| --- | ---- | -------- |
| `useActionState` | handle a form-submit result state type-safely | [Server Actions](/blog/nextjs-16-app-router-cache-components-data-fetching) |
| `useFormStatus` | get pending (sending) on the button side | same |
| `useTransition` | mark heavy updates as "non-urgent" and protect responsiveness | [Core Web Vitals](/blog/core-web-vitals-nextjs-inp-lcp-cls-optimization-guide) |
| `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](/blog/playwright-e2e-testing-production-design-guide)).
- **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` + `useState` fetches with `use` without 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 `addOptimistic` outside 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.

1. Read async during render with **`use(promise)`** and entrust the state to `<Suspense>`.
2. **Create the Promise outside render (Server Component / cache)** to avoid infinite suspend.
3. Show instant feedback with **`useOptimistic`** and let it auto-converge on completion.
4. Always attach **a11y (aria-live / role=status) and security (server is the truth, idempotency).**
5. **Right tool for the job.** TanStack Query for cross-cutting cache, `useOptimistic` for 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.
