3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-12-18

はじめに

12/5に、React19が正式リリースされました。

その中で以下のhooksがstableとしてリリースされたので、使い方など書いていきます。
特に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を使った実装

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

UseActionStateDemo.tsx
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を定義しないと動作しません

実装例

正しい例

useFormStatusDemo.tsx
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秒後に青い文字で、入力したテキストが表示されます。

UseOptimisticDemo.tsx
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は、特に便利で、これから使われる頻度がかなりかなり高そうな機能だなと思いました。

最後に

他にも色々な記事を書いているので、よければ読んでいってください!

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?