🪝

React19でリリースされたhooksを覚えよう

2024/12/18に公開

はじめに

12/5に、React19が正式リリースされました。
その中で以下のhooksがリリースされたので、使い方など書いていきます。
特にuseActionStateは、かなり便利なhookで、これからかなり使われるようになるhookだと思います。

  • useActionState
  • useFormStatus
  • useOptimistic

useActionState

useActionStateは、アクションの実行結果を利用したいときに使うhookです。
アクションを実行させることを目的としたuseTransitionと、ステートを管理すること目的としたuseReducerを合体させているhookです。

実装例

今までの実装

React19以前だと、以下のようにuseStateなどを使って、ローディングの表示やフォームの値の管理などを行い、実装をしていたかと思います。

import React, { useState, useCallback, FormEvent } from "react";

const updateName = async (name: string) => {
  return new Promise<string>((resolve) => {
    setTimeout(() => resolve(name), 3000);
  });
};

export default function UseStateDemo() {
  const [name, setName] = useState<string>("");
  const [isPending, setIsPending] = useState<boolean>(false);

  const handleSubmit = useCallback(
    async (event: FormEvent<HTMLFormElement>) => {
      event.preventDefault();

      const formData = new FormData(event.currentTarget);
      const inputName = formData.get("name") as string;

      setIsPending(true);

      const newName = await updateName(inputName);

      if (newName) {
        setName(newName);
      }

      setIsPending(false);
    },
    []
  );

  return (
    <div className="mt-10">
      <h1 className="flex justify-center text-xl">useActionState</h1>
      <form
        action={submitAction}
        className="space-y-4 p-6 shadow-md max-w-md mx-auto"
      >
        <div className="flex flex-col">
          <input
            type="text"
            name="name"
            className="border border-gray-300 p-2"
          />
        </div>
        <button
          type="submit"
          disabled={isPending}
          className={`w-full py-2 px-4 text-white ${
            isPending
              ? "bg-emerald-400"
              : "bg-emerald-600 hover:bg-emerald-700 focus:ring-emerald-500"
          }`}
        >
          {isPending ? "Updating..." : "Update"}
        </button>
        {isPending && <p>Loading...</p>}
        {name && <p>{name}</p>}
      </form>
    </div>
  );
}

useActionStateを使った実装

さっきと比べて、とても効率的に実装することができているのがわかります。
とても便利!

import { useActionState } from "react";

const updateName = async (name: string) => {
  return new Promise<string>((resolve) => {
    setTimeout(() => resolve(name), 3000);
  });
};

export default function UseActionStateDemo() {
  const [name, submitAction, isPending] = useActionState(
    async (prevState: string, formData: FormData) => {
      console.log("prevState : " + prevState);
      const newName = await updateName(formData.get("name") as string);
      if (!newName) {
        return "";
      }
      return newName;
    },
    "aaa"
  );

  return (
    <div className="mt-10">
      <h1 className="flex justify-center text-xl">useActionState</h1>
      <form
        action={submitAction}
        className="space-y-4 p-6 shadow-md max-w-md mx-auto"
      >
        <div className="flex flex-col">
          <input
            type="text"
            name="name"
            className="border border-gray-300 p-2"
          />
        </div>
        <button
          type="submit"
          disabled={isPending}
          className={`w-full py-2 px-4 text-white ${
            isPending
              ? "bg-emerald-400"
              : "bg-emerald-600 hover:bg-emerald-700 focus:ring-emerald-500"
          }`}
        >
          {isPending ? "Updating..." : "Update"}
        </button>
        {isPending && <p>Loading...</p>}
        {name && <p>{name}</p>}
      </form>
    </div>
  );
}

細かく解説してきます。

const [name, submitAction, isPending] 

・name
ステートの初期値
このコードでは、useActionStateの第二引数の「"prevState"」が入ってきます。

submitAction
アクションを発火させる関数
こちらはFormのアクション属性に渡します。

isPending
実行中であるかどうかの状態を管理するステートです。

useActionState(
 async (prevState: string, formData: FormData) => {
    const newName = await updateName(formData.get("name") as string);
    if (!newName) {
     return "";
    }
    return newName;
 },
 "prevState"
);

useActionStateの内部では、ステート(prevState)とアクション(FormData)を受け取って、新しいステートを返すといったものです。
※アクションは非同期なので、async, awaitが使えます。

第二引数("prevState")は、ステートの初期値となります。

画面の動き

useFormStatus

useFormStatusは、フォームをsubmitした後の処理の状態をユーザーに知らせることができるようにするhookです。
ただし、useFormStatusは、Formコンポーネント内部のコンポーネント内部でuseFormStatusを定義しないと動作しません

実装例

正しい例

import { useFormStatus } from "react-dom";

// こちらのフォーム内部で使用するコンポーネント内で、useFormStatusを定義
function Submit() {
  const { pending } = useFormStatus();
  return (
    <button
      type="submit"
      disabled={pending}
      className="bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
    >
      {pending ? "Submitting..." : "Submit"}
    </button>
  );
}

async function updateName() {
  await new Promise((resolve) => setTimeout(resolve, 3000));
}

function Form({ action }: { action: () => void }) {
  return (
    <form action={action}>
      <Submit />
    </form>
  );
}

export const UseFormStatusDemo = () => {
  return (
    <div className="mt-10 flex justify-center">
      <Form action={() => updateName()} />
    </div>
  );
};

ダメな例

こちらは、form内部のコンポーネントでuseFormStatusを定義していないため、pendingは機能しません。

import { useFormStatus } from "react-dom";

async function updateName() {
  await new Promise((resolve) => setTimeout(resolve, 3000));
}

function Form({ action }: { action: () => void }) {
  const { pending } = useFormStatus();
  return (
    <form action={action}>
      <button
        type="submit"
        disabled={pending}
        className="bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
      >
        {pending ? "Submitting..." : "Submit"}
      </button>
    </form>
  );
}

export const UseFormStatusDemo = () => {
  return (
    <div className="mt-10 flex justify-center">
      <Form action={() => updateName()} />
    </div>
  );
};

useOptimistic

useOptimisticは、楽観的UI更新を実現してくれるhookです。
楽観的UI更新は、実際の処理が終わる前に、ユーザーに完了後の状態を表示するUIのことです。
例としては、X(Twitter)のいいねボタンなどが挙げられます。

実装例

以下の実装では、フォームにテキストを入力し、Submitボタンをクリックすると、最初に赤い文字で入力したテキストが表示され、3秒後に青い文字で、入力したテキストが表示されます。

import { useOptimistic, useState } from "react";

async function updateName(name: string) {
  await new Promise((resolve) => setTimeout(resolve, 3000));
  return name;
}

export const UseOptimisticDemo = () => {
  const [name, setName] = useState("");
  const [optimisticName, setOptimisticName] = useOptimistic("");

  const submitAction = async (formData: FormData) => {
    const newName = formData.get("name") as string;
    setOptimisticName(newName);
    const updatedName = await updateName(newName);
    await setName(updatedName);
  };

  return (
    <div className="mt-10">
      <h1 className="flex justify-center font-bold text-xl">useOptimistic</h1>
      <form
        action={submitAction}
        className="space-y-4 bg-white p-6 shadow-md max-w-md mx-auto"
      >
        <div className="flex flex-col">
          <input
            type="text"
            name="name"
            className="border border-gray-300 "
            disabled={"" !== optimisticName}
          />
        </div>
        <button type="submit" className="bg-blue-500  text-white py-2 px-4  ">
          Submit
        </button>
      </form>
      <p className="mt-4 text-center">
        Your name is:{" "}
        <span className="font-semibold text-red-500">{optimisticName}</span>
        <span className="font-semibold text-blue-500">{name}</span>
      </p>
    </div>
  );
};

まとめ

React19で追加されたhooksは、どれも便利なものばかりですね。
特に、useActionStateは、特に便利で、これから使われる頻度がかなりかなり高そうな機能だなと思いました。

Discussion