---
title: "Async Reactとは？Transition・Suspense・useOptimisticで非同期UIを組む実装【React 19.3】"
url: "https://www.issoh.co.jp/tech/details/11005/"
published: 2026-02-17
updated: 2026-10-21
categories: ["React"]
publisher: "株式会社一創"
---

# Async Reactとは？Transition・Suspense・useOptimisticで非同期UIを組む実装【React 19.3】

Async Reactは、React Conf 2025でReactチームのRicky Hanlon氏が行った講演「Async React Part I／Part II」の名前です。`import`できる単独のAPIではありません。Transition（Actions）・Suspense・楽観的更新を組み合わせ、通信が速ければ読み込み表示を出さず、遅ければ自動で出す画面を、画面側のコードを増やさずに作る書き方を指します。

この記事では、npmの最新安定版React 19.3.1（2026年10月14日公開）を前提に、各APIと、講演デモの部品（`action` propを受け取るボタン・タブ・検索欄）の作り方をコードと実行結果で示します。検証はreact／react-dom 19.3.0と19.3.1をjsdom上で動かして行いました。本文のミリ秒値は検証環境で観測した値で、実ブラウザの描画時間やReactが保証する処理時間ではありません。[19.3.1の変更点](https://github.com/facebook/react/releases/tag/v19.3.1)はServer Componentsの転送処理の改善だけで、await後の包み忘れの結果は19.3.1で測り直しても同じでした。

## まとめ：Async Reactの正体とReact 19.3での書き方の要点

- Async Reactは講演名とワーキンググループ名で、実体は `startTransition`（async関数＝Actions）、`useOptimistic`、`use`＋`<Suspense>` の組み合わせです。
- await後の `set` 呼び出しは、もう一度 `startTransition` で包まないとTransition扱いになりません。その更新で表示済みの `<Suspense>` 配下が中断すると、表示中の内容がfallbackに置き換わります。
- 講演の中心は、ボタンやタブなどの部品が `action` propを受け取り、Transitionと `useOptimistic` を内側で持つ分担です。部品の側で連打の抑止と応答順の逆転への対策をしないと、3回押せば3回送信され、古い応答が入力欄を書き戻します。
- `use` に渡すPromiseはキャッシュが必須です。レンダーのたびに作ると、読み込み中の表示から抜けません。
- asyncなコンポーネントを書けるのは、現時点ではServer Componentsだけです。クライアントでは `use` でPromiseを読みます。

以下、定義、Actions、楽観的更新、データ読み込み、部品の作り方、遅い通信の扱い、Server Components、Next.js、採用しない場面の順に説明します。

## Async Reactの定義：React Conf 2025の講演名とワーキンググループの対象

### 単独のAPIではなく、Transition・Suspense・楽観的更新を束ねた呼び名

React公式ブログの[React Conf 2025振り返り記事](https://react.dev/blog/2025/10/16/react-conf-2025-recap)（2025年10月16日）は、Ricky Hanlon氏の「Async React Part I」「Part II」を、過去10年の積み重ねで何ができるようになったかを示した講演として紹介しています。講演の最終版デモは GitHub の [rickhanlonii/async-react](https://github.com/rickhanlonii/async-react) にあり、READMEには実装側の役割分担が書かれています（デモの `package.json` は react・react-dom に experimental 版を指定しています）。ルーターはナビゲーションを既定でTransitionとして扱い、データ取得層は既定でSuspenseを使います。デザインコンポーネントは `action` propを公開し、楽観的更新と遅延表示の読み込み状態を内側で受け持ちます。

2025年10月8日には GitHub に [reactwg/async-react](https://github.com/reactwg/async-react)（Async React Working Group）が作られました。Discussion #1が挙げる対象は、Transitions／Actions、Suspense／Suspending、Optimistic／Pending の3系統です。重点は、これらの機能をデータ取得・ルーティング・デザインコンポーネントの各ライブラリで使えるようにすることで、ドキュメントや講演による普及も目標に含まれます。議論は誰でも閲覧でき、参加は招待制です。

アプリ開発者は、ライブラリが用意した `action` や Suspense 対応のデータ取得に処理を渡すだけになります。isLoadingフラグを画面ごとに手で管理する書き方からこの分担へ移すのが、Async Reactの中身です。

名前の似たnpmパッケージ「react-async」は、Promiseベースのデータ読み込みライブラリで、この講演とは関係ありません。npmの最新版は2020年3月27日公開の10.0.1です。

### 構成APIと導入バージョンの対応表

| API                           | 役割                  | 安定版で導入 |
| ----------------------------- | ------------------- | ------ |
| useTransition／startTransition | 更新を非緊急として扱う         | 18.0.0 |
| useDeferredValue              | 値の反映を後回しにする         | 18.0.0 |
| async関数のAction                | 通信を含めてTransitionにする | 19.0.0 |
| useOptimistic                 | 完了前に結果を表示する         | 19.0.0 |
| useActionState                | Actionの結果と保留状態を持つ   | 19.0.0 |
| use                           | PromiseやContextを読む  | 19.0.0 |
| <Activity>                    | UIと状態を隠したまま保持する     | 19.2.0 |
| <ViewTransition>              | 非緊急の更新をアニメーションする    | 19.3.0 |

導入バージョンは facebook/react の CHANGELOG と React 19.2／19.3 のリリース記事によります。React 18のままでは、async関数のActionと `useOptimistic`・`use` が使えません。19.3.0で安定化した `<ViewTransition>` がアニメーションするのは、`startTransition` 内の更新、`<Suspense>` の表示切り替え、`useDeferredValue` による更新です。緊急の更新はアニメーションしないため、アニメーションも「どの更新をTransitionにするか」の設計に乗ります。`addTransitionType` による向き別のアニメーションなど、`<ViewTransition>` の具体的な書き方は[React 19.3の新機能](/tech/details/18249/)で解説しています。React 19全体の変更点は[React 19の変更点と最新バージョンの解説](/tech/details/4509/)にまとめています。

## Actionsの実装：startTransitionにasync関数を渡すときのawait後の扱い

### isPendingで保存中を出す基本形

React 19.0.0から `startTransition` はasync関数を受け取れます。この関数をReactは「Action」と呼び、awaitした通信が終わるまでTransitionを続けます。数量をサーバーに保存する例です（`updateQuantity` は保存して確定値を返す関数で、ここでは省略します）。

```
import { useState, useTransition } from 'react';

function Checkout() {
  const [quantity, setQuantity] = useState(1);
  const [isPending, startTransition] = useTransition();

  function handleChange(next) {
    startTransition(async () => {
      const saved = await updateQuantity(next);
      startTransition(() => {
        setQuantity(saved);
      });
    });
  }

  return (
    <>
      <button disabled={isPending} onClick={() => handleChange(quantity + 1)}>+1</button>
      <span>{isPending ? '更新中…' : '数量: ' + quantity}</span>
    </>
  );
}
```

保存に40msかかる関数でボタンを押すと、表示は「更新中…」から「数量: 2」へ直接切り替わりました。`isPending` を自分の `useState` で持つ必要はありません。

### await後の更新を包み直さない場合のfallback再表示（実行結果）

react.dev の `useTransition` リファレンスは、await より後の `set` 呼び出しを別の `startTransition` で包む必要があると明記しています。将来直す予定の既知の制限です。包まなかった場合の違いを、30msで解決するデータを `use` で読む `<Suspense>` 配下で比べました。表示中の結果がある状態で、5msのawaitの後に検索条件を変え、表示を1msごとに記録しています。

| await後の書き方          | データ解決前                 | データ解決直後（約50ms）         | fallbackの確定 |
| ------------------- | ---------------------- | ---------------------- | ----------- |
| setQ(‘b’) をそのまま呼ぶ   | 旧結果のまま（isPending=true） | 読み込み中（isPending=false） | あり          |
| startTransition で包む | 旧結果のまま（isPending=true） | 新しい結果                  | なし          |

包まない書き方では、表示済みの結果が `display: none` で隠され、fallbackが確定しました。この検証では、fallbackへの切り替え直後に `isPending` が false になり、新しい結果の表示より先にボタン側の保留状態が解除されました。新しい結果が出たのはクリックから約350ms後です。React 19.0.0でSuspenseの表示間隔の制御（throttling）が500msから300msに短縮されており、react-dom 19.3.0にもこの値が入っています。ボタンの二重押し防止に `isPending` を使う画面では、包み忘れがそのまま不具合になります。

## useOptimisticの実装：保存完了前の表示と巻き戻りの条件

`useOptimistic` は、Actionの実行中だけ一時的な値を見せるフックです。「いいね」ボタンで、サーバーの応答を待たずに数を増やす例です。postLikeは保存後の確定件数を返すアプリ側の非同期関数で、ここでは実装を省略しています。

```
import { useState, useOptimistic, startTransition } from 'react';

function LikeButton({ likes, saveLike }) {
  const [optimisticLikes, addOptimistic] = useOptimistic(
    likes,
    (current, delta) => current + delta
  );

  function handleClick() {
    startTransition(async () => {
      addOptimistic(1);
      await saveLike();
    });
  }

  return <button onClick={handleClick}>♥ {optimisticLikes}</button>;
}

function Post() {
  const [likes, setLikes] = useState(10);

  async function saveLike() {
    const next = await postLike(likes);
    startTransition(() => setLikes(next));
  }

  return <LikeButton likes={likes} saveLike={saveLike} />;
}
```

このコードでは、クリック直後に「♥ 11」と表示され、保存完了後も11のままでした。`saveLike` から `setLikes` を外すと、表示は「♥ 11」から「♥ 10」へ戻ります。楽観的な値はActionが終わると捨てられ、第1引数の `likes` に置き換わるためです。不具合に見えますが仕様どおりの動きで、確定値で親の状態を更新するまでが実装に含まれます。

Actionの外で更新関数を呼ぶと、開発ビルドのコンソールに次の警告が出ます。

```
An optimistic state update occurred outside a transition or action. To fix, move the update to an action, or wrap with startTransition.
```

フォームの `action` で `useActionState` と組み合わせる書き方は、[React 19のform actionの使い方｜引数はFormData・よくある誤り](/tech/details/2851/)で扱っています。

## useとSuspenseのデータ読み込み：Promiseをキャッシュする理由

### キャッシュ済みPromiseを渡す基本形

`use` はPromiseを受け取り、解決するまでコンポーネントを中断（suspend）させます。中断中は最も近い `<Suspense>` の `fallback` が出て、Promiseが拒否されると最も近いError Boundaryのfallbackが表示されます。react.dev はClient Componentで渡すPromiseについて、再レンダーのたびに同じインスタンスを使えるようキャッシュすることを求めています。

```
import { use, Suspense } from 'react';

const cache = new Map();
function fetchUser(id) {
  if (!cache.has(id)) {
    cache.set(id, fetch('/api/users/' + id).then((res) => {
      if (!res.ok) throw new Error('HTTP ' + res.status);
      return res.json();
    }));
  }
  return cache.get(id);
}

function UserName({ id }) {
  const user = use(fetchUser(id));
  return <p>{user.name}</p>;
}

function Profile({ id }) {
  return (
    <Suspense fallback={<p>読み込み中…</p>}>
      <UserName id={id} />
    </Suspense>
  );
}
```

応答に40msかかる `fetch` に差し替えて実行すると、「読み込み中…」の後にユーザー名へ切り替わりました。isLoadingの分岐はコンポーネントに書いていません。`use` はフックではないため条件分岐の中でも呼べ、詳しくは[Reactのuseフック（use API）の使い方](/tech/details/2989/)で解説しています。

### レンダー中にPromiseを作ったときの読み込み中の固着

次のように、キャッシュを通さずレンダー中に `fetch` を呼ぶ書き方は動きません。

```
function UserName({ id }) {
  const user = use(fetch('/api/users/' + id).then((res) => res.json()));
  return <p>{user.name}</p>;
}
```

解決を待って再レンダーするたびに新しいPromiseが作られ、また中断します。応答に40msかかる `fetch` で実行すると、1.5秒たっても表示は読み込み中のままで、その間に `fetch` が43回呼ばれていました。react-dom 19.3.0の開発ビルドには「A component was suspended by an uncached promise. Creating promises inside a Client Component or hook is not yet supported, except via a Suspense-compatible library or framework.」という警告文（console.error）がありますが、この条件ではコンソールに出ませんでした。エラーが出ないまま画面が止まるので、キャッシュ漏れは警告に頼らずコードで確認してください。

### useEffect内の取得とSuspense対応ライブラリの境界

react.dev の `<Suspense>` リファレンスには、EffectやイベントハンドラーでのデータのフェッチをSuspenseは検知しないと書かれています。`useEffect` で取得して `useState` に入れる既存コードを `<Suspense>` で囲んでも、fallbackは出ません。

自前のMapキャッシュは、無効化や再取得の仕組みを持ちません。本番の画面では、無効化や再取得を自前で書かずに済むSuspense対応ライブラリに任せます。TanStack Query 5.104.5 は `useSuspenseQuery` を公開しており、キャッシュと再検証をライブラリ側に任せたまま `<Suspense>` で待てます。使い方は[TanStack Queryとは？React Queryとの違い・v5の使い方と既定値の落とし穴](/tech/details/10307/)を参照してください。

## action propを受け取る部品の作り方：講演デモのボタン・タブ・検索欄

講演デモの `src/design` には、shadcn/ui形式の部品を包んだ `Button`・`TabList`・`SearchInput` などがあります。画面側は `action` や `changeAction` に関数を渡すだけで、Transitionと楽観的更新は部品の内側に閉じています。デモの書き方を写した部品をReact 19.3.1で動かし、そのまま使うと困る点を測りました。

### Buttonのactionと連打：isPendingによる押下抑止

```
import { useTransition } from 'react';

function Button({ action, children }) {
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() => startTransition(async () => { await action(); })}
    >
      {isPending ? '送信中…' : children}
    </button>
  );
}
```

デモの `Button.jsx` は `isPending` でスピナーを出すだけで、押下は止めていません。`disabled` を外した版で、50msかかる送信処理を5ms間隔で3回クリックすると、表示は1回目の直後から「送信中…」に変わったものの、`action` は3回呼ばれ、件数も3増えました。`disabled={isPending}` を付けると呼び出しは1回です。保存や決済のボタンを部品化するときは、押下の抑止を部品の責任に含めます。

### useOptimisticによる選択値・入力値の即時表示

デモの `TabList` と `SearchInput` は、親から受け取った値を `useOptimistic` に通し、「楽観的な値と親の値が違う間」を保留中とみなします。`isPending` を別に持たずに済む書き方です。

```
import { useOptimistic, startTransition } from 'react';

function SearchInput({ value, changeAction }) {
  const [inputValue, setInputValue] = useOptimistic(value);
  const isPending = inputValue !== value;

  function handleChange(e) {
    const next = e.target.value;
    startTransition(async () => {
      setInputValue(next);
      await changeAction(next);
    });
  }

  return (
    <>
      <input aria-label="検索語" value={inputValue} onChange={handleChange} />
      {isPending && <span>検索中…</span>}
    </>
  );
}
```

react.dev は、Transitionの更新をテキスト入力の制御に使えないとしています。実際、`useState` の更新を `startTransition` で包んで入力欄を制御すると、入力イベントの直後に入力欄の値がいったん空文字へ戻り、次のタスクで「a」になりました。`useOptimistic` を通した上の部品では、入力直後から「a」のままです。楽観的な値は、Transitionの中で更新しても即座に描画されるためです。

親の `changeAction` を「50ms待ってから `startTransition` で検索語を更新する」関数にして「a」「ab」と10ms間隔で入力すると、親の検索語は「ab」に一度だけ切り替わり、「a」の段階は描画されませんでした。進行中のTransitionをReactがまとめて処理するためです。

### 応答順の逆転で入力欄が書き戻される：2つの対策

同じ部品で、「a」の検索に80ms、「ab」の検索に20msかかるようにすると、最後に届いた「a」の結果が採用され、入力欄の表示も「ab」から「a」へ書き戻されました。react.dev の [useTransition リファレンス](https://react.dev/reference/react/useTransition#my-state-updates-in-transitions-are-out-of-order)は、await を含むTransitionでは更新の順序が入れ替わることがあり、将来直す予定の既知の制限だとしています。自前で部品やフックを作る場合、順序の制御は作る側の仕事です。

```
// 対策1：useActionStateで順番に処理する
const [query, dispatch] = useActionState(async (prev, next) => {
  await search(next);
  return next;
}, '');
// <SearchInput value={query} changeAction={(next) => dispatch(next)} />

// 対策2：最新の要求の結果だけを反映する
const latest = useRef(0);
async function changeAction(next) {
  const id = ++latest.current;
  await search(next);
  if (id !== latest.current) return; // 古い応答は捨てる
  startTransition(() => setQuery(next));
}
```

| 書き方            | 最終的な入力欄 | 「ab」の確定（最初の入力から） | 要求の送り方      |
| -------------- | ------- | ---------------- | ----------- |
| 対策なし           | a（書き戻し） | されない             | 並行          |
| useActionState | ab      | 約110ms後          | 直列（前の完了後に次） |
| 最新だけ反映         | ab      | 約100ms後          | 並行          |

「最新だけ反映」でも、「ab」の結果は最初の入力から約50msで届いたのに、確定は約100ms後でした。遅い「a」の要求が終わるまで、まとめて処理されるTransitionが閉じないためです。検索のように最新の結果だけが要る入力は「最新だけ反映」、保存のように全件を順に送る必要がある操作は `useActionState` を選びます。`useActionState` はフォームの `action` と組み合わせることが多く、その書き方は[React 19のform actionの使い方｜引数はFormData・よくある誤り](/tech/details/2851/)で扱っています。

### 速い通信で読み込み表示を出さない仕組みはCSSの遅延

デモのREADMEは、通信が150ms未満なら読み込み表示を出さず同期的に動き、150msを超えると読み込み表示を自動で出す、と説明しています。この閾値はReactの機能ではありません。デモの `src/index.css` で、保留中の表示を遅らせて出しています。

```
.pending {
  opacity: 0;
  transition-delay: 200ms;
  transition-property: opacity;
  transition-duration: 200ms;
}
.pending.long {
  transition-delay: 1.5s;
}
.pending.isPending {
  opacity: 1;
}
```

`isPending` はAction開始と同時に true になるので、そのままスピナーを出すと、40msで終わる保存でも一瞬だけ表示がちらつきます。検索欄のスピナーは1.5秒、タブのきらめき（shimmer）は300ms遅らせるなど、デモは部品ごとに遅延を変えています。部品を自作するときも、保留中の表示に遅延を入れるところまでが「速ければ出さない」の実装です。

## 遅い通信で古い表示が残る問題：Working Groupの回答と未解決の論点

Transitionは表示済みの内容をfallbackに戻さないため、通信が遅いと、切り替えたタブの下に前のタブの一覧が残ります。Working Groupの[Discussion #4](https://github.com/reactwg/async-react/discussions/4)（2025年11月14日）で、devongovett氏が「一定時間後にfallbackへ戻したい」と質問し、Ricky Hanlon氏が回答しています。回答の要点は次のとおりです。

- fallbackに戻すより、一定時間後に今の内容を薄くするか、きらめき（glimmer）を重ねる。内容を表示したまま待てるため、クリックしようとした瞬間に一覧がfallbackへ置き換わることを避けられる。
- 概念的に別のページへ移るなら、fallbackを出したうえで先読みする。ホバーからクリックまでの時間を使う方法と、よく使うタブを非表示の `<Activity>` で先に描画しておく方法が挙がっている。
- Actionの中で `await Promise.race([prefetch, timeout(50)])` のように短く待ってから遷移する手もあるが、ちらつきの原因になりうる。

`<Activity>` はReact 19.2.0で安定版に入ったコンポーネントで、使い方は[React 19.2の新機能｜Activity・useEffectEvent・cacheSignal・PPRの使い方と19.2.8までの修正](/tech/details/9427/)で解説しています。

一方、[Discussion #6](https://github.com/reactwg/async-react/discussions/6)（2025年11月21日）は2026年10月21日時点で回答が0件です。Unsplashの開発者が、Suspense対応のルーターでは保留中のActionがサイト内の移動を妨げると報告し、Actionを中断する方法や、移動後も裏で続ける方法を尋ねています。数秒以上かかる処理をActionにすると、この問題を自分で抱えることになります。

## asyncコンポーネントはServer Components限定：クライアントで書いたときのエラー

### サーバーでawait、クライアントでuseに渡す分担

Server Componentsでは、コンポーネント自体を `async function` にしてレンダー中に `await` できます。react.dev のServer Componentsリファレンスには、ページに必須のデータはサーバーでawaitし、優先度の低いデータはPromiseのままクライアントへ渡す例があります。次のコードはその例をもとに、Suspenseのimportやkeyなどを補ったものです。

```
// Server Component
import { Suspense } from 'react';
import db from './database';
import Comments from './Comments';

async function Page({ id }) {
  // 本文はサーバーで待つ
  const note = await db.notes.get(id);
  // コメントはawaitせず、Promiseのまま渡す
  const commentsPromise = db.comments.get(note.id);
  return (
    <div>
      {note.title}
      <Suspense fallback={<p>コメントを読み込み中…</p>}>
        <Comments commentsPromise={commentsPromise} />
      </Suspense>
    </div>
  );
}
```

```
// Comments.js（Client Component）
'use client';
import { use } from 'react';

export default function Comments({ commentsPromise }) {
  const comments = use(commentsPromise);
  return comments.map((c) => <p key={c.id}>{c.text}</p>);
}
```

本文の表示はコメントの取得を待ちません。クライアント側の `Comments` に40msで解決するPromiseを渡すと、「コメントを読み込み中…」の後に一覧へ切り替わりました。サーバーとクライアントの境界の決め方は[React Server Components（RSC）とは？](/tech/details/4003/)で詳しく扱っています。

### async Client Componentのエラーメッセージと原因

同じ書き方をクライアントですると失敗します。`async function` のコンポーネントをクライアントで描画したところ、fallbackのまま次のエラーが出ました（`%s` にはコンポーネント名が入ります）。

```
%s is an async Client Component. Only Server Components can be async at the moment. This error is often caused by accidentally adding `'use client'` to a module that was originally written for the server.
```

メッセージのとおり、サーバー用に書いたファイルへ `'use client'` を足したときによく起きます。直し方は、async関数をServer Componentに残してPromiseを渡すか、クライアント側で `use` に置き換えるかのどちらかです。`useEffect` のコールバック自体を async にするのは別の誤りです。Promiseがクリーンアップ関数の位置に返るため、React 19.3.0では「must not return anything besides a function, which is used for clean-up.」というエラーがコンソールに出ます。Effectの中でasync関数を定義して呼び出してください。

## Next.js App Routerのloading.jsとSuspense境界の範囲

Next.js App Routerでは、`loading.js` を置くと同じセグメントの `page.js` と配下が自動で `<Suspense>` に包まれ、`loading.js` の内容がfallbackになります。同じセグメントの `layout.js`・`template.js`・`error.js` は包まれないため、レイアウトが `cookies()` などのランタイムデータや未キャッシュのデータを読むと、そのレイアウトにはfallbackが効きません。ページ全体を1つの `loading.js` で待たせるより、遅いデータだけを `<Suspense>` で個別に囲む方が、先に出せる部分が増えます。

Next.jsの[サポートポリシー](https://nextjs.org/support-policy)では16.xがActive LTSで、npmの最新版は2026年10月15日公開の16.4.2です。16系で最小構成のアプリを動かす手順は[Next.jsとは？16系App Routerで最小アプリを動かす手順と採用判断](/tech/details/17373/)にあります。

## Async Reactに書き換えない方がよい画面の条件

既存コードをすべて書き換える必要はありません。次の条件に当てはまる画面は、今の書き方を残す方が安全です。

- **入力欄の値そのもの**：`useState` をTransitionで更新して入力欄を制御しないでください。入力欄の表示は即時の更新か `useOptimistic` で持ち、入力に連動する重い一覧には一覧側に `useDeferredValue` を使います。
- **React 18から上げられないアプリ**：async関数のAction、`useOptimistic`、`use` は19.0.0からです。18では `startTransition` に同期関数しか渡せず、手書きのisLoading管理は残ります。
- **TanStack QueryのuseQueryで安定している画面**：取得・キャッシュ・エラー表示が既に動いていて、Transitionと連携して古い結果を残す必要もなければ、`use` へ置き換えるためだけの改修は勧めません。Suspenseに寄せたいときは、画面単位で `useSuspenseQuery` に切り替える方が差分が小さく済みます。
- **行ごとに独立した保存中表示が要る一覧**：react.dev は、進行中のTransitionが複数あると現在はまとめて処理されると記しています。`useOptimistic` の値に保留フラグを持たせても、この制限は回避できません。React 19.3.0で2行を別々に保存したところ、保存の早い行も、遅い行の保存が終わるまで保存中の表示が残りました。行ごとに完了を見せたい一覧は、今の書き方を残します。
- **数秒以上かかる処理**：Actionを中断する公式の手段はなく、Working GroupのDiscussion #6も未回答です。大きなファイルの書き出しのような処理は、Actionにせずジョブとして進捗を別に持ちます。

新しく作る画面では、データ取得をSuspense対応ライブラリかServer Componentsに任せ、更新をActionにする形を最初から選ぶのが手戻りの少ない進め方です。業務システムを新規に作る場合は、部品の `action` propの規約を最初に決めておくと、画面ごとの読み込み表示の書き方が揃います。設計から任せたい場合は、一創の[フルスクラッチ開発](https://www.issoh.co.jp/service/system/fullscratch/)で相談を受けています。

## よくある質問

### Async Reactの書き方を使うにはReactのどのバージョンが必要ですか？

async関数を `startTransition` に渡すActions、`useOptimistic`、`useActionState`、`use` は、いずれもReact 19.0.0（2024年12月5日）で安定版に入りました。React 18.0.0にあるのは、同期関数だけを受け取る `useTransition`／`startTransition` と `useDeferredValue` までです。2026年10月21日時点のnpm最新安定版は19.3.1（10月14日公開）です。`<ViewTransition>` は19.3.0で安定化しました。新規に始めるなら19.3系を選んでください。

### setStateは非同期ですか？awaitで更新後の値を待てますか？

待てません。`useState` の `set` 関数の戻り値は `undefined` で、React 19.3.0で実行しても同じでした。`await setCount(1)` と書いても更新の完了は待たず、同じ関数内の変数は古い値のままです。更新は次のレンダーで反映されます。通信の完了まで保存中を表示したいなら、処理をActionにして `useTransition` の `isPending` を使うのが、react.dev のリファレンスが示す方法です。

### useフックとasync/awaitはどう使い分けますか？

レンダー中の `await` は、asyncなServer Componentでしか使えません。Client Componentでは `use` でPromiseを読みます。`use` は名前に反してフックではなく、if文の中でも呼べます。ただしClient Componentに渡すPromiseはキャッシュされている必要があります。サーバーでPromiseを作ってpropsとして渡し、クライアントの `use` で受け取る分担が、react.dev の示す組み合わせです。

### useTransitionとuseDeferredValueはどう使い分けますか？

更新する状態の `set` 関数に自分でアクセスできるなら `useTransition`、propsやカスタムフックから来た値を遅らせたいなら `useDeferredValue` です。react.dev の `useTransition` リファレンスも、`set` 関数に触れない場合は `useDeferredValue` を試すよう案内しています。検索欄のように入力値そのものが絡む場合は、入力は即時に更新し、結果の一覧だけを `useDeferredValue` で遅らせます。

### Async Reactの書き方でINPは改善しますか？

重い再レンダーが操作への反応を塞いでいる画面なら改善が見込めます。Transitionとして扱われた更新は、入力など他の更新が来ると中断されるためです。レンダー自体が軽い画面では差が出ません。INPは2024年3月12日からCore Web Vitalsの指標で、web.devは200ms以下を良好としています。導入前後のINPを、Chrome DevToolsのPerformanceパネルと実ユーザーの計測値（CrUXなど）で比べてください。

## 関連記事

- [React 19とは？18からの変更点・新機能とReact Compiler・最新バージョンを解説](/tech/details/4509/)
- [Reactのuseフック（use API）の使い方——React 19のPromise・Context読み取りを解説](/tech/details/2989/)
- [React Server Components（RSC）とは？サーバー・クライアントの違いと使い分けを実装例で解説](/tech/details/4003/)
- [React 19のform actionの使い方｜引数はFormData・よくある誤り](/tech/details/2851/)
- [React 19.3の新機能｜ViewTransition・Fragment ref・browser()の実装と19.2からの更新手順](/tech/details/18249/)

---

出典: [Async Reactとは？Transition・Suspense・useOptimisticで非同期UIを組む実装【React 19.3】](<https://www.issoh.co.jp/tech/details/11005/>)（株式会社一創）
