React 19のuseでPromiseとコンテキストを読む方法
React 19 の use を使うと、Promise の結果とコンテキストの値を、描画中に1つの API で読めます。Promise の待ち時間は <Suspense> が、失敗は Error Boundary が受け持ちます。そのため、コンポーネントの中に「読み込み中」や「エラー」の state を持たずに済みます。エフェクトでデータを取得して state に入れるやり方では、これらを自分で管理する必要がありました。2つの書き方の違いは、公式リファレンスにある、useEffect で取得する書き方との比較で確かめられます。
ただし、use に渡す Promise は、描画のたびに作り直してはいけません。筆者は検証環境で、キャッシュした Promise を渡す書き方と、描画のたびに Promise を作る書き方を TypeScript(.tsx)で書き比べました。そして、画面の変わり方と、作られた Promise の数を調べました。あわせて、try-catch の中で呼んだときと、if のあとでコンテキストを読んだときの動きも確かめました。
検証環境は Ubuntu 24.04、Node.js 22.22.0 です。使ったのは react / react-dom 19.3.1、@types/react 19.3.0、typescript 6.0.3、esbuild 0.28.2、jsdom 29.1.1、eslint 10.13.0、eslint-plugin-react-hooks 7.1.1、typescript-eslint 8.72.0 です。.tsx を tsc で型チェックしてから esbuild で変換し、jsdom の上で描画しました。GIF は Playwright 1.56.1 の Chromium で撮りました。データの取得には、決まった時間がたつと解決する偽の関数を使っています。
use で読めるのは Promise とコンテキスト
use は、React 19 で加わった、描画中にリソースを読むための API です(React 19 の発表記事)。公式リファレンスの説明によると、Promise を渡すと、そのコンポーネントは Promise が解決するまで中断(suspend)します。解決すると、use はその値を返します。
名前はフックと同じく use で始まりますが、フックではありません。フックと違い、ループや if の中でも呼べます。ただし、呼べるのはコンポーネントかフックの中だけです(リファレンスの注意事項)。
Promise を読む:待ち時間は Suspense、失敗は Error Boundary が受け持つ
検証では、アルバムの一覧を返す偽のデータ取得関数を用意しました。getAlbums() は、呼ぶたびに新しい Promise を作り、100ms 後に解決します。fetchAlbums(key) は、同じキーなら同じ Promise を返す簡単なキャッシュです。これは、リファレンスにある、Client Component で Promise をキャッシュする例と同じ作りです。
// 同じキーなら、同じ Promise を返す
const cache = new Map<string, Promise<Album[]>>();
export function fetchAlbums(key: string): Promise<Album[]> {
if (!cache.has(key)) cache.set(key, getAlbums());
return cache.get(key)!;
}
一覧を出すコンポーネントは、受け取った Promise を use に渡すだけです。
export function Albums({ albumsPromise }: { albumsPromise: Promise<Album[]> }) {
const albums = use(albumsPromise);
return (
<ul>
{albums.map((album) => (
<li key={album.id}>{album.title} ({album.year})</li>
))}
</ul>
);
}
親では、Albums を <Suspense> で包み、キャッシュした Promise を渡します。ボタンは、親を再描画させるためのものです。
export function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>再描画 {count}</button>
<Suspense fallback={<Loading />}>
<Albums albumsPromise={fetchAlbums('/albums')} />
</Suspense>
</>
);
}
描画してから 300ms 待ち、そのあと「再描画」ボタンを3回押しました。押したあとも、毎回 300ms 待っています。それぞれの時点で画面に出ていた文字は、次のとおりです。
$ node run.mjs ./out/CachedApp.mjs
描画した直後: 再描画 0 読み込み中…
300ms 後 : 再描画 0 Neko Walk (2024) Paw Print (2025)
クリック1回目の直後: 再描画 1 Neko Walk (2024) Paw Print (2025) / 300ms 後: 再描画 1 Neko Walk (2024) Paw Print (2025)
クリック2回目の直後: 再描画 2 Neko Walk (2024) Paw Print (2025) / 300ms 後: 再描画 2 Neko Walk (2024) Paw Print (2025)
クリック3回目の直後: 再描画 3 Neko Walk (2024) Paw Print (2025) / 300ms 後: 再描画 3 Neko Walk (2024) Paw Print (2025)
作った Promise=1 fallback を出した回数=1
fallback の「読み込み中…」が出たのは最初だけで、Promise が解決すると一覧に置き換わりました。再描画しても同じ Promise が渡るので、3回とも fallback は出ていません。作った Promise も1個のままです。
Promise が失敗したときは、リファレンスにある、Error Boundary でエラーを表示する例のとおり、エラーはいちばん近い Error Boundary に届きます。100ms 後に失敗する Promise を用意し、クラスで書いた Error Boundary の内側で読ませました。表示は次のように変わりました。
ErrorApp: 直後=読み込み中… / 300ms 後=エラー: 読み込みに失敗しました
描画のたびに Promise を作ると、再描画のたびに読み込み中に戻る
次に、親の fetchAlbums('/albums') を getAlbums() に変えました。親が描画されるたびに、新しい Promise ができる書き方です。それ以外は同じ手順で動かしました。
$ node run.mjs ./out/UncachedApp.mjs
描画した直後: 再描画 0 読み込み中…
300ms 後 : 再描画 0 Neko Walk (2024) Paw Print (2025)
クリック1回目の直後: 再描画 1 読み込み中… / 300ms 後: 再描画 1 Neko Walk (2024) Paw Print (2025)
クリック2回目の直後: 再描画 2 読み込み中… / 300ms 後: 再描画 2 Neko Walk (2024) Paw Print (2025)
クリック3回目の直後: 再描画 3 読み込み中… / 300ms 後: 再描画 3 Neko Walk (2024) Paw Print (2025)
作った Promise=4 fallback を出した回数=4
ボタンを押すたびに、一覧が消えて「読み込み中…」に戻りました。作った Promise は4個、fallback も4回です。データの中身は変わっていないのに、再描画のたびに読み込みからやり直しています。
次の GIF アニメーションは、この2つの書き方を Chromium で上下に並べ、「再描画する」ボタンを3回押した様子です。上の「① キャッシュした Promise を渡す」が fetchAlbums('/albums') を渡す版、下の「② 描画のたびに新しい Promise を渡す」が getAlbums() を渡す版です。待ち時間は、目で追えるように 600ms にしました。

②は、リファレンスの、Client Component で Promise をキャッシュする説明で、避けるべきだとされている書き方です。描画中に新しい Promise を作ると、再描画のたびに Suspense の fallback が出ると説明されています。
use を呼ぶコンポーネントの中で Promise を作ると、表示されないことがある
Promise を、use を呼ぶコンポーネントの中で作る書き方も試しました。
// use を呼ぶコンポーネントの中で Promise を作る
function Albums({ ms }: { ms: number }) {
const albums = use(getAlbums(ms));
return <p>{albums.length}件</p>;
}
act を使わずに描画し、2秒間の画面の様子を見ました。待ち時間が 100ms のときは、2秒たっても「読み込み中…」のままでした。この実行では、そのあいだに Promise が23個作られています。描画が中断するたびに React が最初から描画し直し、そのたびに新しい Promise が作られて、また中断する、ということをくり返したためです。リファレンスの、Promise が作り直される理由の解説にも、マウント前に中断した描画では state が残らず、やり直すたびに Promise が作り直されると書かれています。
$ node run-inrender.mjs 100
500ms 後: 読み込み中… 作った Promise=8
1000ms 後: 読み込み中… 作った Promise=13
2000ms 後: 読み込み中… 作った Promise=23
待ち時間を 0ms にすると、表示まで進みました。ただし、この実行では、表示までに Promise が38個作られています。また、0ms のときだけ、次のメッセージが console.error に出ました。100ms のときは出ていません。このメッセージは発表記事にも載っていて、原因と直し方はリファレンスのトラブルシューティングで説明されています。
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.
なお、eslint-plugin-react-hooks 7.1.1 は、②の書き方にも、use を呼ぶコンポーネントの中で作る書き方にも、何も出しませんでした。
描画のたびに Promise を作って use に渡すと、lint は通りますが、再描画のたびに画面が「読み込み中」に戻ったり、いつまでも表示されなかったりします。検証では、コンソールにメッセージが出ない場合もありました。
Promise の用意のしかたについて、リファレンスの、use に渡す Promise はキャッシュが必要だという注意は、キャッシュ、Suspense に対応したフレームワーク、Server Component のいずれかから渡すよう勧めています。同じページの、Promise が作り直される理由の解説では、イベントハンドラーやルートのローダー、Server Component など、描画より前の段階で作るのが理想だとしています。フレームワークや Server Component と組み合わせた形は、検証環境では動かしていません。
use を try-catch で囲まない
use は、try-catch の中では呼べません(リファレンスの注意事項)。試しに、まだ解決していない Promise を読む use を、try-catch で囲みました。
function Albums({ albumsPromise }: { albumsPromise: Promise<Album[]> }) {
let text: string;
try {
text = `${use(albumsPromise).length}件`; // try-catch の中で呼んだ
} catch (e) {
text = `catch: ${(e as Error).message}`;
}
return <p>{text}</p>;
}
すると、React が内部で使う「中断の合図」を、catch が受け取ってしまいました。Promise が解決したあとも、画面は次の表示のままでした(メッセージが長いので、最初の2文だけ写します)。
catch: Suspense Exception: This is not a real error! It's an implementation detail of `use` to interrupt the current render.
console.error には、「use was called from inside a try/catch block.」で始まるメッセージも出ました。lint も、この行を React Hook "use" cannot be called in a try/catch block というエラーにします。失敗に備えるときは、try-catch ではなく、さきほどの例のように Error Boundary で包みます(リファレンスのトラブルシューティング)。
コンテキストは if のあとでも読める
コンテキストを渡すと、use は useContext と同じように値を返します。違いは呼べる場所です。useContext はコンポーネントのトップレベルで呼ぶ必要がありますが、use は if や for の中でも呼べます(リファレンスの、use でコンテキストを読む例)。発表記事の例にならって、早めに return したあとでコンテキストを読む形を書きました。
export function Heading({ children }: { children?: ReactNode }) {
if (children == null) {
return null;
}
const theme = use(ThemeContext);
return <h1 className={theme}>{children}</h1>;
}
値を dark にした ThemeContext の内側で、同じ Heading を children なし、ありの順に描画しました。1回目は何も描画されず、2回目は <h1 class="dark">ねこ</h1> になりました。lint も何も出しません。
同じ位置で useContext を呼ぶ版も用意したところ、lint はエラーにしました。
./src/Theme.tsx
19:17 error React Hook "useContext" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? react-hooks/rules-of-hooks
この useContext 版も、検証の手順では同じ表示になり、実行時のエラーや警告は出ませんでした。ただ、発表記事は、この例を useContext ではうまくいかない書き方として示しています。useContext 版はフックのルールに反するので、条件のあとでコンテキストを読みたいときは use を使います。
コンテキストに Promise を入れて、use を2回呼ぶ
コンテキストの値に Promise を入れておけば、props で受け渡さなくても、非同期のデータを配れます。読む側では、まずコンテキストから Promise を取り出し、次にその Promise の中身を読みます。そのため、use を2回呼びます(リファレンスの、コンテキストから Promise を読む例)。
function Profile() {
const userPromise = use(UserContext); // まずコンテキストから Promise を読む
if (!userPromise) return <p>ログインしていません</p>;
const user = use(userPromise); // 次に Promise の中身を読む
return <h1>{user.name}</h1>;
}
<Suspense> で包んで描画すると、直後は「読み込み中…」と出て、300ms 後には <h1>Mike</h1> になりました。この例でも、2つ目の use は if のあとで呼んでいます。
use に渡す Promise は、描画の外で作るか、キャッシュから受け取ります。そうしておけば、待ち時間は <Suspense> に、失敗は Error Boundary に任せられます。コンテキストは、条件のあとでも use で読めます。





