EY-Office ブログ

React19のuseOptimistic (+ useActionState) Hookを使ってみた

今回もReact 19 Betaの新機能を解説です。3回目はActionsの useOptimistic Hookです。

ドキュメントには、

useOptimistic は、何らかの非同期アクションが進行中の間だけ、異なる state を表示するための React フックです。ある state を引数として受け取ってそのコピーを返しますが、ネットワークリクエストなどの非同期アクションが実行中の場合に異なる値を返すことができます。現在の state とアクションへの入力を受け取り、アクション実行中に使用される楽観的 state を返すような関数を渡します。

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

useOptimisticとは

useOptimisticのわかりやすい例は、カウンター付きのいいねボタンではないでしょうか。

いいねボタンの動作は、

  1. いいねボタンが押される
  2. サーバー(バックエンド)のいいねボタン処理が呼び出される
  3. サーバー内に保存されているいいね数が+1される
  4. サーバーからは最新のいいね数が戻る
  5. 画面に最新のいいね数を表示する

ここで、2〜4の間にいいね数をどうするかですが、

  • 5まで変更されないのは、ボタンを押した反応が無いので良くないですね
  • ローディング・アニメーション等を表示するのは大げさかもしれませんね
  • 取りあえず現在ブラウザーに表示されているいいね数+1を表示するのが良さそうですね、5で表示されるいいね数が+2だとしても問題ないですよね

このような機能を実装するのに使えるのが useOptimistic Hookです。

今回のサンプルコード

今回もジャンケンです😃、以前のブログ React19のActions, useActionState Hookを使ってみたで作ったジャンケンアプリの改良版です。

下の画面画像のように、ポンボタンを押した時点で、コンピューターの手、勝敗に⌛ ...が表示されまれ。そしてアクションが完了した時点で正式な値が表示されます。

コード解説

React19のActions, useActionState Hookを使ってみたのコードをチョコチョコ変更しています。

準備

  • JyankenType.ts

ジャンケンの手に未定義、勝敗に判定中の状態を追加しました。

export const Te = {
  Guu: 0,
  Choki: 1,
  Paa: 2,
  Undefine: 3     // ← 手が判らない状態の追加
} as const;

・・・

export const Judgment = {
  Draw: 0,
  Win: 1,
  Lose: 2,
  Judging: 3      // ← 判定中の状態の追加
} as const;
  • ScoreBox.tsx

⌛を表示するためにデータを追加しました。

export default function ScoreBox ({scores}: ScoreBoxProps )  {
  const teString = ["グー", "チョキ", "パー", "⌛ ..."];                // ← 追加
  const judgmentString = ["引き分け", "勝ち", "負け", "⌛ 判定中 ..."];  // ← 追加

Jyanken.tsx

メインのコードです。ここでuseOptimisticが使われます。

  • ① ponAction関数にuseOptimisticを加えたoptimisticPonAction関数の定義
  • ② 楽観的な更新を行うaddOptimisticScores関数を呼出します、引数にはフォームデータ・オブジェクトから取得したジャンケンの手を渡しています
  • ③ ここで、本来の処理ponAction関数を呼出します
  • ④ useActionStateの呼出し、アクションがoptimisticPonActionに変更されています
  • ⑤ 楽観的な更新を行う関数定義
    • 引数は、
      • state: 現在のステート
      • action: ②で渡された引数、ここではジャンケンの手
    • Stateの先頭に楽観的な更新値を連結
      • human: 人間の手はactionで渡された値
      • computer: コンピューターの手は判らないのでTe.Undefine
      • judgment: 勝敗も判らないのでJudgment.Judging
  • ⑥ useOptimisticの呼出し、
    • テンプレートにはStateの型、アクションの引数の型
    • 引数は、現在のStateと楽観的な更新を行う関数
  • ⑦ ScoreBoxコンポーネントには楽観的なState、optimisticScoresを渡します
"use client";
import ScoreBox from '../_components/ScoreBox';
import JyankenBox from '../_components/JyankenBox';
import { useActionState, useOptimistic } from 'react';
import { ScoreType, Te, Judgment } from '../_libs/JyankenType';
import { ponAction } from '../_libs/JyankenAction';

type JyankenProps = {
  defaultScores: ScoreType[]
}

export default  function Jyanken ({defaultScores}: JyankenProps) {
                                                                    // ↓ ①
  const optimisticPonAction = async (prevState: ScoreType[], formData: FormData):
      Promise<ScoreType[]> => {
    addOptimisticScores(Number(formData.get("te")) as Te);          // ← ②
    return await ponAction(prevState, formData);                    // ← ③
  }

  const [scores, formAction] =                                      // ↓ ④
    useActionState<ScoreType[], FormData>(optimisticPonAction, defaultScores);

  const updateOptimistic = (state: ScoreType[], action: Te) =>      // ← ⑤
    [{human: action, computer: Te.Undefine, judgment: Judgment.Judging,
      comment: null}, ...state];

  const [optimisticScores, addOptimisticScores] =                   // ← ⑥
    useOptimistic<ScoreType[], Te>(scores, updateOptimistic);

  return (
    <>
      <h1>じゃんけん ポン!</h1>
      <JyankenBox formAction={formAction} />
      <ScoreBox scores={optimisticScores} />                      {/* ← ⑦ */}
    </>
  );
}

まとめ

通常のアクション処理は useActionState を使い、ローディング・アニメーション等を表示すれば良いと思われます。
しかし、ゲームやカウンター付きのいいねボタンなどのように直ぐにフィードバックを表示したほうが良い場合は、useOptimistic Hookを使うと良いと思います。

useActionStateを使ったコードなら

  • アクションに楽観的更新の呼出しを追加したアクションの作成
  • 楽観的更新のStateを戻す関数の作成
  • useOptimistic 呼出し
  • 楽観的Stateへの切り替え

くらいの僅かな変更で対応できます。useActionStateuseOptimistic の関係が少し判りにくいところもありますが定型パターンなので一度作れば理解できると思います。

- about -

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