💫

【React 19】 use Hookのアンチパターン

2025/01/12に公開

はじめに

今回は、React 19から搭載されたuseのアンチパターンについて解説します。

これまでにできなかった、Promiseを子コンポーネントに渡せたりと、便利で画期的なHookなのですが、使い方には、一定の注意が必要なので、そこを見ていきたいと思います。

use とは

usePromiseContextなどのリソースから値を読み取るためのAPIです。

以下のように、Contextから値を読み取ったり、Promise解決ができたりするHookです。

import { use } from 'react'

const Example = () => {
  const theme = use(themeContext)
  const samplePromise = use(samplePromise)
}

詳細は公式ドキュメントをご参照ください。
https://ja.react.dev/reference/react/use#caveats

React 18との比較

前述の通り、useはかなり便利なので、以下のツイートのようにReact18と比べた際に非常に非同期処理の記述を簡潔にすることができます。

https://x.com/sugiyama_yo/status/1877884391277646295

アンチパターン

しかし、便利だからといって、乱用するのは危険を伴います。
公式ドキュメントにも、注意点が上げらております。

公式では3つの注意点が上げらていますが、特に重要なのが以下です。

  • サーバコンポーネントでデータをフェッチする際は、use よりも async と await を優先して使用してください。async と await は await が呼び出された地点からレンダーを再開しますが、use はデータが解決した後にコンポーネントを最初からレンダーします。
  • クライアントコンポーネントでプロミスを作成するよりも、なるべくサーバコンポーネントでプロミスを作成してそれをクライアントコンポーネントに渡すようにしてください。クライアントコンポーネントで作成されたプロミスは、レンダーごとに再作成されます。サーバコンポーネントからクライアントコンポーネントに渡されたプロミスは、再レンダー間で不変です。こちらの例を参照してください。

そして、この注意点を理解すると、先ほど紹介したツイートのような記述は無限レンダリングというバグを生むことが理解できるのではと思います。

どういうことか、詳細に見ていきましょう。

アンチパターン① 無限レンダリング

クライアントコンポーネントで作成されたプロミスは、レンダーごとに再作成されます。サーバコンポーネントからクライアントコンポーネントに渡されたプロミスは、再レンダー間で不変です。

重要なのは上記の記述です。
そして、以下は、ClientComponentでuseを使用しPromise作成・解決を図ろうとしているコードになります。
先ほど紹介したツイートのように一見正しく動作しそうですが、ツイートのコードのように無限レンダリングとなります。

'use client'

import { Suspense, use } from 'react'

const getName = async () => {
  await new Promise((resolve) => setTimeout(resolve, 3000))

  const randomName =
    Math.random().toString(36).substring(2, 7) +
    Math.random().toString(36).substring(2, 7)

  return randomName
}

const Hoge = () => {
  console.log('render')
  const name = use(getName())

  return <Suspense fallback={<div>loading...</div>}>{name}</Suspense>
}

export default Hoge

useによるPromise作成・解決をClientComponentで行うと以下のようになります。

  1. Promise作成
  2. useによるPromise解決
  3. レンダリング
  4. ClientComponentなので、レンダリング後Promiseが再作成される
  5. useによるPromise解決
  6. レンダリング
    ・・・

上記の様になるため、無限レンダリングが発生します。

アンチパターン② 不要なレンダリング

以下はServerComponentでuseを使用する例です。
ちなみに、useはHookですが、ServerComponentでも使用できます。

ただし、このような使い方も公式ではやめるよう記載があるため、やめましょう。
理由は以下の通りです。

async と await は await が呼び出された地点からレンダーを再開しますが、use はデータが解決した後にコンポーネントを最初からレンダーします。

要するにServerComponentでは、従来通り、async・awaitによるPromise解決を使用するということになります。

実際に不要なレンダリングが発生するか確認してみましょう。
まずはasync・awitによるPromise解決です。

import { Suspense } from 'react'

const getName = async () => {
  await new Promise((resolve) => setTimeout(resolve, 3000))

  const randomName =
    Math.random().toString(36).substring(2, 7) +
    Math.random().toString(36).substring(2, 7)

  return randomName
}

const Hoge = async () => {
  console.log('render')
  const name = await getName()

  return <Suspense fallback={<div>loading...</div>}>{name}</Suspense>
}

export default Hoge

以下の画像の通り、ログ出力は3回されているようです。
3回なのはPromise解決されるまではSuspenseのfallbackがレンダリングされるため、以下のようになるためです。

  1. 初回SSR(1回目のログ出力)
  2. Promise作成
  3. Promise解決する間fallback表示(fallbackレンダリングによる2回目のログ出力)
  4. Promise解決後、コンテンツ更新のため再レンダリング(3回目のログ出力)

console1

次にuseによるPromise解決をする例です。

import { Suspense, use } from 'react'

const getName = async () => {
  await new Promise((resolve) => setTimeout(resolve, 3000))

  const randomName =
    Math.random().toString(36).substring(2, 7) +
    Math.random().toString(36).substring(2, 7)

  return randomName
}

const Hoge = () => {
  console.log('render')
  const name = use(getName())

  return <Suspense fallback={<div>loading...</div>}>{name}</Suspense>
}

export default Hoge

以下にあるように、余分に1回レンダリングされているのがわかると思います。
console2

  1. 初回SSR(1回目のログ出力)
  2. Promise作成
  3. Promise解決する間fallback表示(fallbackレンダリングによる2回目のログ出力)
  4. Promise解決後、コンテンツ更新のため再レンダリングをかける(3回目のログ出力)と同時に、useがPromise解決後にコンポーネントを再レンダリングしにいく(4回目のログ出力)

上記のようになるため、従来のasync/awaitによる解決が望ましいということです。
※ 4の処理順が、どちらが先か私が完全に把握していないため、正確性に欠ける部分はありますが、余分にレンダリングされるということだけ理解しておいてください

おまけですが、上記のようなPromise解決のコードはより、シンプルにすることができるので、それを紹介して解説は終わろうと思います。

サンプルのリファクタコード

この場合は、SuspenseがPromiseを解決してくれると捉えて良いと思います。

import { Suspense } from 'react'

const getName = async () => {
  await new Promise((resolve) => setTimeout(resolve, 3000))

  const randomName =
    Math.random().toString(36).substring(2, 7) +
    Math.random().toString(36).substring(2, 7)

  return randomName
}

const Hoge = () => {
  return <Suspense fallback={<div>loading...</div>}>{getName()}</Suspense>
}

export default Hoge

もしくは、より直接的に以下のようにすることもできます。
今回はNext.js 15のカナリー版でコード作成したのですが、これからはJSX内で直接awaitさせることもできるようになりますので、覚えておくとよいかと思います。

const getName = async () => {
  await new Promise((resolve) => setTimeout(resolve, 3000))

  const randomName =
    Math.random().toString(36).substring(2, 7) +
    Math.random().toString(36).substring(2, 7)

  return randomName
}

const Hoge = async () => {
  return <div>{await getName()}</div>
}

export default Hoge

おわりに

useのアンチパターンを紹介しました。
このようにReactには公式ドキュメント内でアンチパターンや注意点などを紹介しているので、いかに公式ドキュメントの読み込みが重要かがわかるのではないでしょうか。
useEffectは「本当に必要なのか?」というページが単体であったりします)

https://react.dev/learn/you-might-not-need-an-effect

公式ドキュメントは理解できるまで読み込むのがベストですが、一読するだけでも違うので、一目通しておくと良いと思います。

参考文献

https://ja.react.dev/reference/react/use

https://x.com/sugiyama_yo/status/1877884391277646295

Discussion