1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React 19の新しい`use` APIとPromise処理の変化

Posted at

React 19では新フックuseが導入され、レンダー中にPromiseやContextなどの「リソース」を直接読み取れるようになりました (What’s new in React 19 - Vercel) (What’s new in React 19 - Vercel)。これによりコンポーネントのレンダリング中にPromiseの結果を待つ(サスペンドする)ことが可能になり、従来よりも簡潔に非同期データを扱えます。例えば、サーバーまたは他の場所で取得したPromiseをコンポーネントに渡し、コンポーネント内でuse(promise)と呼ぶと、ReactはそのPromiseが解決するまでコンポーネントのレンダリングを一時停止します (React v19 – React) (React v19 – React)。Promiseが解決済みであれば即座に値を返し、未解決であれば内部的にそのPromiseをthrowして最近接の<Suspense>にキャッチさせます (Promises across the void: Streaming data with RSC) (Promises across the void: Streaming data with RSC)(エラーならErrorとしてthrowされ、エラーバウンダリで捕捉)。ReactはこのthrowされたThenable(Promise)を検知して該当コンポーネントを「サスペンド中」とマークし、親の<Suspense>はフォールバックUI(ローディング表示など)をレンダリングします (React v19 – React) (use – React)。Promiseが解決するとReactはサスペンドしていたコンポーネントを再レンダリングし、useは解決結果の値を返すためコンポーネントは本来のUIを表示できます (use – React) (use – React)。この仕組みにより、Reactはレンダリングの途中で一時停止・再開が可能となり(React 18以前はレンダリング中に同期的に処理するしかなく、データ待ちには副作用フックや外部状態管理が必要でした (Promises across the void: Streaming data with RSC))、非同期処理のコードが大幅に簡潔になりました。

useフックの特筆すべき点はレンダリング中のPromise待機を公式にサポートしたことです。従来はSuspenseを使ったデータ取得に特殊なパターン(例:レンダリング中にPromiseをthrowするようなライブラリ)や事前データ取得が必要でしたが、React 19ではuseにPromise(Thenable)を渡すだけで同等のことが行えます (React v19 – React) (What’s new in React 19 - Vercel)。さらにuseは他のHookと異なり条件分岐やループの中でも呼び出し可能です (What’s new in React 19 - Vercel)(呼び出し順序に縛られず、必要なときにだけ実行される)。これはuseがコンポーネントの状態を持つ通常のHookというより、「リソースを読む」ための特殊APIで、内部でPromiseやContextの参照状況を管理するためです。そのためuseにはいくつか制約があり、新たに生成されたPromiseをそのまま渡す場合は適切にキャッシュする必要があります (React v19 – React)。例えばクライアントコンポーネント内で毎回新しいPromiseを作ってuseすると毎レンダリングで異なるPromiseを投げ続け無限ループになるため、Reactは「レンダー中に生成された未キャッシュのPromise」に対して警告を出します (React v19 – React)。この問題への対処として、React 19ではReact.cacheといったAPIが追加され、関数の戻り値(Promise)をキャッシュして安定化させる仕組みも提供されています。Next.jsなどのフレームワークやSWRなどのライブラリも内部でPromiseのキャッシュ戦略を採用し、この制約を回避しています (React v19 – React) (Upgrading: Single-Page Apps | Next.js)。

Next.jsにおけるReact 19の統合と活用
Next.js(特にApp Routerを用いる13以降のバージョン)は、React 19のこれら新機能を深く統合しています。Next.jsはReact Server Components (RSC)を採用しており、サーバー上で実行されるコンポーネントとクライアント上で実行されるコンポーネントを組み合わせてページを構築します。React 19のuseフックやSuspenseを活用することで、データフェッチをサーバーで開始しつつレンダリングをブロックしないというパターンが可能になりました。具体的には、サーバーコンポーネントからクライアントコンポーネントへPromiseをプロップ経由で渡し、クライアント側でそのPromiseをuseで消費するという手法です (use – React) (use – React)。Next.jsのAppディレクトリでは以下のようなコードパターンが推奨されています。例えばレイアウト(サーバーコンポーネント)でユーザーデータを取得する際、awaitで待たずにまずPromiseを得てしまい、それをコンテキストプロバイダなどのクライアントコンポーネントに渡します (Upgrading: Single-Page Apps | Next.js) (Upgrading: Single-Page Apps | Next.js):

// app/layout.tsx – サーバーコンポーネント
export default function RootLayout({ children }) {
  const userPromise = getUser(); // awaitせずにPromise取得
  return (
    <html>
      <body>
        {/* クライアント側のUserProviderにPromiseを渡す */}
        <UserProvider userPromise={userPromise}>
          {children}
        </UserProvider>
      </body>
    </html>
  );
}

そして、その受け取り側であるUserProviderコンポーネント(クライアントコンポーネント)ではuseフックを使ってPromiseから値を読み出し、Context経由で子孫のクライアントコンポーネントに提供します。

// app/user-provider.tsx – クライアントコンポーネント
"use client";
import { createContext, use } from 'react';
const UserContext = createContext(null);
export function UserProvider({ children, userPromise }) {
  const user = use(userPromise);  // Promiseが解決するまでサスペンド
  return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
}
export function useUser() {
  const context = use(UserContext); // Contextもuseで取得可能
  if (context === null) throw new Error("useUser must be within UserProvider");
  return context;
}

こうしておけば、任意の深さのクライアントコンポーネントでuseUser()を呼び出してユーザーデータを参照できます。上記の例では、UserProvider内でuse(userPromise)を実行した時点で、もしuserPromiseがまだ解決していなければこのコンポーネントはサスペンド状態になります。Reactはこれを検知してUserProvider直上のSuspense境界にフォールバックUIを表示します。Next.jsではレイアウトやページでSuspenseを利用するか、またはloading.tsxを用意することでフォールバックを定義できます。上の例ではレイアウト内で明示的にSuspenseを使っていませんが、Next.jsはクライアントコンポーネントのサスペンスによる「部分的なハイドレーション」を自動で処理します。実際、Next.js公式ドキュメントでも「Promiseを消費するコンポーネント(上記例ではProfileなど)はサスペンドされ、これによって部分的なHydrationが可能になる。JavaScriptのロードが終わる前にストリーム配信されたプリレンダリングHTMLを確認できる」と説明されています (Upgrading: Single-Page Apps | Next.js)。つまり、Promiseの解決を待たずにまずサーバーが持つ他の部分のHTMLをストリーミングレンダリングし、クライアント側では未解決データの部分だけHydrationを遅らせることで全体の表示を迅速に行うのです。

Server ComponentからClient ComponentへPromiseを渡す内部実装(Next.jsとReactの連携)
サーバーコンポーネントからクライアントコンポーネントにPromiseを直接渡す仕組みは、React 19とNext.jsのストリーミング機構が連携して実現されています。その内部では次のような手順が行われています (Promises across the void: Streaming data with RSC) (Promises across the void: Streaming data with RSC):

  1. サーバー側でのID付与とプレースホルダー: サーバーがコンポーネントツリーをレンダリングする際、クライアントコンポーネントのプロパティに未解決のPromiseが渡されていると分かった時点で、React/Next.jsはそのPromiseに一意の内部IDを割り当てます (Promises across the void: Streaming data with RSC)。そして実際のPromiseオブジェクトの代わりに、そのIDを示すプレースホルダー情報をクライアントに送ります。言い換えれば、サーバーは「ここに将来ID=Xのデータが入る」という印を埋め込んだJSONペイロードないしマーカーを生成し、クライアントコンポーネントのpropsとして渡します。

  2. サーバーからのHTMLストリーミング送信: Next.jsはReactのストリーミングSSRを用いて、生成したHTMLを逐次クライアントに送信します。この時点で未解決のデータ部分については<Suspense>のフォールバックUI(ローディング表示)や空コンテナのみがHTMLに含まれます (use – React)。重要なのは、サーバーはレスポンスをすぐには閉じずストリームを開いたままにすることです (Promises across the void: Streaming data with RSC)。これにより、サーバーは他の部分のHTMLを先に送りつつ、バックエンドでPromiseの解決を待ち続けることができます。Next.jsではこの「ストリーミング応答」を実現するためにNode.jsのストリームやWeb Streams APIを活用し、サーバーコンポーネントの結果を逐次クライアントに流せるようになっています(React 18で導入されたsuspense対応のrenderToPipeableStream/renderToReadableStreamを活用)。

  3. クライアントでの一時サスペンド: クライアントはまず送られてきたプリレンダリングHTMLを受け取りレンダリングしますが、Promiseが未解決の部分については上記の通りフォールバックUIしかない状態です。並行して、Next.jsはサーバーから送られたもう一つの情報源(RSCペイロード)を使ってクライアントコンポーネントの初期プロップを再現します。このときプロップ内のPromiseは実体ではなく**「Promise ID」**として渡されています。Next.jsのクライアント側実装は、このIDに対応する「未解決のPromise」をクライアント上で生成または参照し、コンポーネントにその代用Promiseを与えます (Promises across the void: Streaming data with RSC) (Promises across the void: Streaming data with RSC)。結果としてクライアントコンポーネント(例えば先のMessageUserProvider)はuse(promise)を実行しますが、そのPromiseはまだ解決されていないためクライアント側でもサスペンドが発生します。クライアントではReactのHydrationプロセス中にサスペンドが起きるため、対応するSuspense境界(既にHTMLで描画済みのフォールバック)を維持し、コンポーネント本体の描画(およびイベントバインドなどのHydration)を一時停止します。この「部分的なHydration停止」により、ユーザーにはページ全体の骨格や他の即時表示可能な部分が先に見える一方、未解決データに依存するUI部分は読み込み中の表示のままとなります。

  4. Promise解決とデータ注入: サーバー側でバックエンド処理が進行し、やがてPromiseが解決されると(例えばデータベースから結果を取得できた等)、サーバーは保持していたストリームを通じて追加のHTML片として解決データを送信します (Promises across the void: Streaming data with RSC)。具体的には、先ほど割り当てたPromise IDと解決結果の値を含むスクリプトタグをHTMLストリームの末尾に挿入します (Promises across the void: Streaming data with RSC) (Promises across the void: Streaming data with RSC)。Next.jsでは、この埋め込まれたスクリプトが実行されると内部で対応するPromiseを見つけ出し、その値でresolve(完了)させる処理が行われます (Promises across the void: Streaming data with RSC)。先述のようにクライアント側ではPromise IDに紐づくダミーのPromiseを持っていますから、サーバーから届いたデータでそれが完了状態になり、本物の値が得られます。

  5. クライアントコンポーネントの再開(ハイドレーション完了): Promiseが解決されたことで、停止していたクライアントコンポーネントのレンダリングが再開されます。ReactはSuspenseによるサスペンドから復帰し、use(promise)が今回解決済みの値を返すため、クライアントコンポーネントは本来のUIを表示できます (use – React) (use – React)。例えばMessageコンポーネントなら「Here is the message: ○○」という実データ入りの内容がフォールバックと置き換わります(Context経由の場合も、コンテキスト値が更新され子コンポーネントが再レンダリングされます)。以上の一連の流れにより、サーバーからクライアントへ未解決Promiseを渡しつつ非同期データを逐次ストリーム配信することが実現されています。ポイントはサーバー側でレスポンスを保持したまま追加データを後送りできるストリーミング機構と、クライアント側でPromiseの境界をSuspense/水際でハンドリングするHydration機構です。React 19+Next.jsはこれらを密接に連携させ、開発者は単にPromiseを渡してuseするだけで裏側の複雑な同期を意識せずに済むようになっています。

React 19側の変更点(新しいレンダリング方式とデータ管理)
以上の機能を支えるために、React 19自体にも様々な改善が加えられました。まず大きいのはレンダリングモデルの進化です。useフックの導入により、Reactは「レンダー中に途中停止して結果待ち→再開」という従来になかった処理が可能となりました (Promises across the void: Streaming data with RSC)。これはFiber(仮想DOM再計算の仕組み)レベルでの拡張であり、Suspenseを用いた非同期レンダリングが一段と強力になっています。React 18でもSuspenseによるサーバーストリーミングは実験的に導入されていましたが、React 19では公式に安定化し、useと組み合わせることでフレームワークなしの純React環境でもPromiseを使ったデータ取得とストリーミングUI表示が可能になりました (use – React) (Mastering React 19: Exploring the Latest Features of the New React ...)(もっとも実際にはNext.jsのようなフレームワークが統合して使うケースが主流です)。またReact 19ではReact Server Components (RSC) が正式にサポートされました (What’s new in React 19 - Vercel)。RSCはReact 18で提案された仕組みですが、React 19で安定版となり、Next.jsもこれを前提にしたApp Routerを提供しています。RSCの導入に伴い、Reactは「サーバーで実行されるコンポーネントツリーをシリアライズしてクライアントに送り、クライアントでそれを復元・一部実行する」という新たなレンダリング手法(通称「React Flight」プロトコル)を実装しています。今回の「Promiseを未解決のまま渡す」機能も、このRSCプロトコル上でシリアライズ可能なデータ型としてThenable(Promise)を扱えるように拡張された結果と言えます (use – React) (use – React)。具体的には先述のようにPromiseの解決値がJSONシリアライズ可能であることを前提にID割り当て・逐次送信する実装がReact/Next.jsに組み込まれました (use – React)。さらにデータ管理の面では、React 19はuseフックを円滑に機能させるためリソースのキャッシュ戦略も強化しています。公式にはuseで読むPromiseはキャッシュされたものを使うよう求められており (React v19 – React)、React自体もReact.cache()というヘルパーを提供して関数の戻り値Promiseをメモ化できるようにしました。Next.js側でもfetch関数を自動でキャッシュ&再利用したり(デフォルトで同じリクエストは結果を保持する) (Upgrading: Single-Page Apps | Next.js)、use cacheディレクティブによって特定の関数やコンポーネントの出力をキャッシュする独自拡張を用意するなど (Directives: use cache | Next.js) (Directives: use cache | Next.js)、React 19の仕組みを補完する形でデータの管理・再利用を最適化しています。

まとめると、React 19ではuseフックの導入によってPromiseを直接レンダーで扱いサスペンドできる新機構が生まれ、Server ComponentsやSuspenseと組み合わせることでサーバー発のデータを非同期ストリーミングでクライアントUIに反映する道が開けました。Next.jsはこのReact 19の機能を統合し、Promiseをawaitせず渡すことでサーバーレンダリングをブロックしないデータ取得パターンを実現しています (use – React)。その内部ではPromiseにIDを振って逐次データを送り、クライアントでSuspend/Resumeさせるという高度な仕組みが動いています (Promises across the void: Streaming data with RSC) (Promises across the void: Streaming data with RSC)。開発者はReact 19とNext.jsの提供するAPI(use, Suspense, Server/Clientコンポーネント区分など)を利用することで、複雑な非同期処理をシンプルかつ高効率に実装できるようになったと言えるでしょう。

参考資料: React公式ブログおよびドキュメント (React v19 – React) (use – React)、Next.js公式ドキュメント (Upgrading: Single-Page Apps | Next.js)、Vercelブログ (What’s new in React 19 - Vercel)、技術記事 (Promises across the void: Streaming data with RSC) (Promises across the void: Streaming data with RSC)などに、これらの仕組みに関する詳細な解説があります。特にEd Spencer氏のブログ記事は、Next.jsによるRSCストリーミングの内部処理(PromiseのID割当てやスクリプトによる値注入)について踏み込んだ説明を行っており参考になります (Promises across the void: Streaming data with RSC) (Promises across the void: Streaming data with RSC)。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?