EY-Office ブログ

React19のuse Hookを使うと事でSuspenseが書きやすくなった

React 19 Betaがリリースされましたね! このブログでもReact19の新機能を解説して行こうと思います。

今回は、use Hookです。

公式ドキュメントには、以下のように書かれています。

use はプロミス (Promise) やコンテクストなどのリソースから値を読み取るための React フックです。

use というシンプルな名前、 何なんでしょうか?

react19use Bing Image Creatorが生成した画像を使っています

use Hookとは

use Hookには大きく2つの目的で使われます。1つ目はコンテクスト(Context)の読み取りです。そのためには、以前からuseContext Hookがありましたが、今回のuseも同じ目的で使えます。

ただしuseContextはHooksの基本ルール通りに、関数コンポーネントのトップレベルで使う必要があります。if文の中では使えませんでした。
しかし今回のuseは、if文の中でも使えます。これで従来の不便が改善されましたね。

2つ目は、プロミス (Promise) との組み合わせです。今回はサンプルコードを使って、この機能を解説します。

React 19 Betaの使い方(暫定版)

React 19 Betaの使い方は、React 19 Beta Upgrade GuideのInstallingに書かれています。npm install react@beta react-dom@betaでベータ版をインストールすればよいようです。TypeScriptで使うには型定義が必要で、このドキュメントに書かれていますが、私の環境では上手く行きませんでした。なぜでしょうか??

そこで@types/reactに含まれているcanary.d.tsを使う事で新しいHooksが使えるようにしました。vite-env.d.tsファイルにreact/canaryを追加することでVS Codeでcanary.d.tsが参照されるようになります。

/// <reference types="vite/client" />
/// <reference types="react/canary" />  // ← 追加

Suspenseを思い出してみましょう

Suspenseに付いては、以前 React 18への予習シリーズ:Suspenseを復習しよう ブログに書きました。要約すると

  • Suspense以前は
    • fetch等でのデータ取得は適切なライフサイクルを管理する
      • 通常useEffect内で起動する
    • 取得したデータはStateに格納
    • 通信中(Loading…)管理は自前でState管理
  • Suspenseを使うと
    • ライフサイクル管理はSuspenseが行ってくれる
      • したがって取得したデータはState管理は不要で、通常のローカル変数に格納できる
    • 通信中(Loading…)管理はSuspenseが行ってくれる
    • データ取得はSuspensに対応した通信ライブラリー等を使う必要がある

Suspenseは通信などの非同期処理を含む、コンポーネントが簡単に作れます。

ただし、Suspenseを使うにはSuspensに対応した通信ライブラリー等が必要でした。従来はSWRTanStack Query (旧React Query)を使う必要があしました(さらに、これらのライブラリーはSuspenseと同等の機能も持っていました😅)。

これらのライブラリーはキャッシュ機能も持っており有能なライブラリーですが、JavaScript標準のfetch等が使えないのは不便でした。
fetchを使うには自前でSuspense用に通信プロミスをThrowするコードが必要になります。→ 参考記事ブログ

use + Suspense のサンプルコード

いつものジャンケンアプリです。😃

ジャンケンの対戦結果はStateではなく、json-serverを使ったバックエンドで管理します。

  • ① fetchを使ったバックエンドからのデータ取得用Promiseを戻す関数
    • データを戻す関数ならconst scores = await res.json();になりますが、ここではawaitがないのでPromiseを戻しています
    • 通信中(Loading…)がわかるようにsleepで0.5秒待ち時間を入れています
  • ② fetchを使ったバックエンドにデータを送る普通の関数
  • ③ Suspenseに括られる対戦結果表示コンポーネント
    • ここで、use Hookが使われています、引数はPromiseを戻すgetScores()関数です
  • ④ ジャンケンを行ってもState等の変化がないので location.reload() を使って強制的に画面の再表示を行っています(手抜きです😅)
  • ⑤ Suspenseタグ、ShowScoreBoxコンポーネントを実行します
    • getScoresからthrowされたPromiseが実行中(通信中)はfallback=が表示されます
    • 通信が終了しPromiseが完了すると、ShowScoreBoxコンポーネントが再実行されます。このときは通信結果があるのでthrowは起きず、ScoreBoxコンポーネントが実行され対戦結果が表示されます
import { Suspense, use, useReducer } from 'react';
import { Te, ScoreType, Jjudgment } from './JyankenType';
import JyankenBox from './JyankenBox';
import ScoreBox from './ScoreBox';

const BackendURL = "http://localhost:8000/jyanken";
const sleep = (sec: number) => new Promise(resolve => setTimeout(resolve,
  sec * 1000));

const getScores = async (): Promise<ScoreType[]> => {   // ← ①
  const res = await fetch(BackendURL);
  await sleep(0.5);
  const scores = res.json();
  return scores;
}

const postScore = async (score: ScoreType) => {         // ← ②
  await fetch(BackendURL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(score)
  });
}

function ShowScoreBox() {                              // ← ③
  const scores = use(getScores());
  return (<ScoreBox scores={scores} />);
}

export default function App() {
  const pon = async (human: Te) => {
    const computer: Te = Math.floor(Math.random() * 3);
    const judgment: Jjudgment = (computer - human + 3) % 3;
    const score = {human: human, computer: computer, judgment: judgment};
    await postScore(score);
    location.reload();                               // ← ④
  }

  return (
    <>
      <h1>じゃんけん ポン!</h1>
      <JyankenBox actionPon={te => pon(te)} />
        <Suspense fallback={<p>⌛ Downloading scores...</p>}>   {/* ← ⑤ */}
          <ShowScoreBox />
        </Suspense>
     </>
  );
}

まとめ

Susupenseは上にも書いたように、非同期処理を宣言的に書ける素晴らしいものです。しかし今までは、SWRやTanStack Queryのようなライブラリーを使わないと実現できませんでしたが。
use が導入されるとSusupenseが標準のJavaScript + Reactだけで実装できるようになるので、まだSusupenseを使ってない方はReact19がリリースされたら使ってみてくさい。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ