Menu

React useフックの使い方:レンダリング中にPromiseとコンテキストを読む

useは、レンダリング中にPromiseやコンテキストの値を読みます。Promiseなら、データが届くまでサスペンドし、最も近いSuspenseのフォールバックを表示します。ほかのフックと違い、if文やループの中でも実行できます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

use は、コンポーネントのレンダリング中にPromiseやコンテキストから値を読むReact 19のAPIです。use(promise) はPromiseが解決するまでコンポーネントを一時停止し、その間は最も近い <Suspense> のフォールバックを表示します。use(context) は useContext のように動きますが、if やループの中に置けます。

下の fetchUser 関数は本物のリクエストの代わりです。

最初のレンダリングでは1秒間「Loading...」が表示され、その後プロフィールが表示されます。User 2をクリックすると、新しいPromiseが保留中の間、フォールバックが戻ってきます。Profile は独自の読み込み中のstateを持たず、データがすでにあるかのように書かれています。

use(promise)の仕組み

Profile が use(userPromise) を呼ぶと:

  • Promiseが解決済みなら、use はその値を返し、レンダリングが続きます。
  • まだ保留中なら、Reactは Profile のレンダリングを止め、その上にある最も近い <Suspense> のフォールバックを表示します。Promiseが確定すると、Reactはもう一度 Profile をレンダリングし、今度は use が値を返します。
  • rejectされたなら、Reactは代わりに最も近いエラーバウンダリを表示します。

Reactは結果をPromiseオブジェクトそのものに覚えておきます。だからこそ、次のレンダリングでもPromiseは同じオブジェクトでなければならないのです。

次のプロフィールを読み込む間、前のプロフィールを画面に残したいなら、Promiseをトランジションの中でセットします。するとReactは、フォールバックをもう一度表示する代わりに待ってくれます。

<button onClick={() => startTransition(() => setUserPromise(fetchUser(2)))}>User 2</button>

Promiseを作る場所

ここがつまずきやすい部分です。読む側のコンポーネントの中でfetchを呼ぶのは自然に見えますが、動きません。

// Do not do this
function Profile({ id }) {
    const user = use(fetchUser(id)); // a new promise on every render
    return <p>{user.name}</p>;
}

レンダリングのたびに fetchUser が呼ばれ、新しい保留中のPromiseが得られます。Profile はサスペンドします。そのPromiseが解決すると、Reactはもう一度 Profile をレンダリングし、それがまた保留中のPromiseを作り、またサスペンドします。コンポーネントは決してデータを表示せず、ネットワークタブはリクエストで埋まります。

Promiseは、読む側のレンダリングのたびに再実行されない場所で作ってください。

  • 親で、stateに保つ(最初の例のように)か、イベントハンドラで作ります。
  • モジュールのトップレベルで、データがpropsに依存しないとき:コンポーネントの上に const configPromise = fetchConfig();。
  • 引数をキーにしたキャッシュで、同じidなら同じPromiseを返すようにします。
  • フレームワークで:Server ComponentはPromiseをpropとしてクライアントコンポーネントに渡せ、ルートのローダーも同じことをします。use が想定しているのはこのケースです。

次はキャッシュ版です。コンポーネントがidでデータを求めつつ、安定したPromiseを受け取れます。

3つの都市を順にクリックし、すでに開いた都市に戻ってください。コンソールには都市ごとに1回だけリクエストが記録され、一度見た都市は、Promiseがすでに解決しているのでフォールバックなしに表示されます。if (!cache.has(city)) の確認を削除すると、コンポーネントはまた永遠にサスペンドするようになります。

本物のキャッシュには、エントリを期限切れにする方法も必要です。データライブラリやフレームワークがそれを扱ってくれるので、ほとんどのアプリはそこからPromiseを受け取ります。

use(context)はifの中でも使える

use(SomeContext) は useContext(SomeContext) と同じ値を返します。違いは呼べる場所です。ほかのすべてのフックはトップレベルで、すべてのレンダリングで同じ順番で実行しなければなりません(フックのルール)。use は、早期returnの後、条件分岐の中、ループの中でも呼べます。

ボタンをクリックして2つの分岐を切り替えてください。use(UserContext) を useContext(UserContext) に置き換えると、フックがあるレンダリングでは実行され、別のレンダリングでは実行されないことになるので、コードはフックのルールに反します。

それでも use には制限があります。呼べるのはコンポーネントのレンダリング中か、カスタムフックの中だけです。イベントハンドラやエフェクトでは、レンダリング中にすでに得た値でコンテキストを読んでください。

エラー:try/catchではなくエラーバウンダリ

Promiseがrejectされると、エラーはレンダリングのエラーと同じように、最も近いエラーバウンダリに送られます。

Post 2を開くと、バウンダリがエラーメッセージを表示します。もう一度Post 1を開くと動きます。key={id} がバウンダリに投稿ごとの新しいstateを与えるので、古いエラーを表示し続けることがないからです。

保留中やrejectされた use を try/catch で捕まえることはできません。Promiseが保留中の間、use は特別な値を投げてレンダリングを中断し、その周りの catch はそれを飲み込んでしまいます。捕まえた値は「Suspense Exception: This is not a real error!」で始まるメッセージのエラーで、開発中はReactが「use was called from inside a try/catch block」とも記録します。エラー画面の代わりに代わりの値を表示したいなら、下へ渡す前にPromiseでrejectを処理してください。

const postPromise = fetchPost(id).catch(() => 'This post could not be loaded.');

useとuseEffectでのデータ取得の比較

クライアントの古典的なパターンは、エフェクトでデータを取得し、3つのstateを持つものです。

function Profile({ id }) {
    const [user, setUser] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        let ignore = false;
        fetchUser(id).then(
            (u) => !ignore && setUser(u),
            (e) => !ignore && setError(e),
        );
        return () => {
            ignore = true;
        };
    }, [id]);

    if (error) return <p>Error</p>;
    if (!user) return <p>Loading...</p>;
    return <p>{user.name}</p>;
}
useEffectuse
リクエストが始まるとき最初のレンダリングが画面に出た後Promiseが作られたとき。コンポーネントのレンダリング前のこともある
読み込み中の状態自前の if (!user)最も近い <Suspense>
エラー自前の error state最も近いエラーバウンダリ
競合状態自分で防ぐ(ignore)コンポーネントは常に渡されたPromiseを読む
Promiseを作るものコンポーネント親、キャッシュ、フレームワーク

エフェクト版はそれだけで完結しているので、フレームワークのないアプリでは今も最もよく使われる方法です。詳しくはデータの取得のページで扱っています。use が真価を発揮するのは、コンポーネントより上の何かが早めにリクエストを始め、Promiseを下へ渡すときです。

よくある質問

Reactのuseフックは何をするものですか?

use(resource) はPromiseやコンテキストの値を返します。Promiseの場合、解決するまでコンポーネントはサスペンドし、最も近い <Suspense> がフォールバックを表示します。rejectされると、代わりに最も近いエラーバウンダリが表示されます。

useは条件付きで呼べますか?

はい。use は、if 文やループの中、早期 return の後でも呼べる唯一のフックです。それでもコンポーネントかフックから呼ぶ必要があり、イベントハンドラやエフェクトからは決して呼べません。

コンポーネント内のuse(fetch())が永遠にループするのはなぜですか?

レンダリングのたびに新しいPromiseが作られるからです。コンポーネントはそれでサスペンドし、解決するとReactがもう一度レンダリングし、そのレンダリングがまた保留中のPromiseを作ります。Promiseはレンダリングの外で作ってください。親のstate、モジュールのトップレベル、キャッシュ、フレームワークのローダーなどです。

useをtry/catchで包めますか?

いいえ。Promiseが保留中のとき、use は例外を投げてレンダリングを中断し、それを捕まえると壊れてしまいます。rejectされたPromiseはエラーバウンダリで扱うか、Promiseに .catch() を付けて代わりの値に解決させてください。

useEffectでのデータ取得をuseに置き換えるべきですか?

use には、1回作って再利用されるPromiseが必要で、それを与えてくれるのはフレームワークやデータライブラリです。普通のクライアントアプリでは、エフェクトでの取得(またはTanStack Queryのようなライブラリ)が今もよく使われ、問題ありません。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める