🦔

React19のuseActionStateを使ってみた

2024/12/30に公開

動作環境

  • React:19.0.0

参考

ランニングページです。(サイトの詳細は気が向いたら作ります)
https://sample-react-hooks.vercel.app/#useActionState

前書き

現在(2024/12/30)大学2年生でインターンにも行ってないガチガチの初心者なのでそこら辺、参考にする人は気にかけてください。

今回はReact + viteで作っています、初期設定(npm create vite@latest)とかは省きます。そこら辺は親切な人が色々挙げているのでみていただければ。

もし、プロのエンジニアの方が見ていらしたら「ここはこんな感じで書いた方がいいよー」とか優しく教えていただけると幸いです。

内容

下記は今回主に解説するコンポーネントの親コンポーネントです。
(useStateは別の記事でまとめるつもりなのでそちらに譲ります。)

interface pageProps {
  userName: string;
  age: number;
  comment: string;
}

const useActionStatePage: React.FC<pageProps> = (props: pageProps) => {
  const [formState, setFormState] = useState<pageProps>(props);

  const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    return setFormState({ ...formState, userName: e.target.value });
  };

  const handleAgeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    return setFormState({ ...formState, age: parseInt(e.target.value) });
  };

  const handleCommentChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    return setFormState({ ...formState, comment: e.target.value });
  };

  return (
    <ActionForm
      formState={formState}
      handleAgeChange={handleAgeChange}
      handleCommentChange={handleCommentChange}
      handleNameChange={handleNameChange}
    />
  );
};

export default useActionStatePage;

interfaceのpagePropsで値が書かれていますが、今回はこのコンポーネントの上から下記のデータを渡しています。

const formData = {
  userName: "",
  age: 0,
  comment: "",
};

本題のコンポーネントです。(冗長なためCSSを省いています)

import React, { useActionState } from "react";
import { submitForm } from "../../lib/submit";

interface formState {
  userName: string;
  age: number;
  comment: string;
}

interface formActionProps {
  formState: formState;
  handleNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  handleAgeChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  handleCommentChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

const ActionForm = ({
  formState,
  handleNameChange,
  handleAgeChange,
  handleCommentChange,
}: formActionProps) => {
  const [message, formAction, isPending] = useActionState(submitForm, null); //重要

  return (
    <form
      action={formAction} //重要!!
      name="formData"
      id="useActionState"
    >
      <div className="mb-4">
        <label>Name:</label>
        <input
          type="text"
          value={formState.userName}
          name="userName"
          onChange={(e) => handleNameChange(e)}
        />
      </div>
      <div className="mb-4">
        <label>Age:</label>
        <input
          type="number"
          value={formState.age}
          name="age"
          onChange={(e) => handleAgeChange(e)}
        />
      </div>
      <div className="mb-4">
        <label>Comment:</label>
        <input
          type="text"
          value={formState.comment}
          name="comment"
          onChange={(e) => handleCommentChange(e)}
        />
      </div>
      <div className="mb-4 text-center">
        {isPending && <p>Submmitting...</p>}
        {message?.state ? (
          <p>{message.message}</p>
        ) : (
          <p>{message?.message}</p>
        )}
      </div>
      <div>
        <button type="submit">
          submit
        </button>
      </div>
    </form>
  );
};

export default ActionForm;

実物は先にもあげたようにランニングページをみていただければ...
https://sample-react-hooks.vercel.app/#useActionState

さて、useActionStateが下記の部分で宣言されています

const [message, formAction, isPending] = useActionState(submitForm, null);

ここでは例としてuseActionState([引数1] , [引数2])として説明します。

  • message : useActionStateで扱う変数です。formActionで指定された関数の返り値が格納されます。[引数2]の部分が初期値となるためmessageの初期値はnullです。
  • formAction : useActionStateで扱う関数です。この場合では実行された時に[引数1]に設定した関数にmessageの値と<form></form>内のデータが渡されます。
  • isPending : この値は処理中か否かを格納しています。簡単にいうとformActionが実行中はtureとなります。逆に実行していなければfalseとなります。

下記がsubmitForm関数です。

"use server";

interface QueryData {
  get(key: string): string | null;
}

interface response {
  state: boolean;
  message: string;
}
interface response {
  state: boolean;
  message: string;
}

export async function submitForm(
  // @ts-ignore
  state: response | null,
  queryData?: QueryData
): Promise<response> {
  const isValidUserName = (userName: string | null | undefined) => {
    if (!userName || typeof userName !== "string") {
      return false;
    }
    return true;
  };
  const isValidAge = (age: number | null | undefined) => {
    if (!age || typeof age !== "number") {
      return false;
    }
    return true;
  };
  const isValidComment = (comment: string | null | undefined) => {
    if (!comment || typeof comment !== "string") {
      return false;
    }
    return true;
  };

  await new Promise((resolve) => setTimeout(resolve, 1000));

  const userName = queryData?.get("userName");
  const age = queryData?.get("age");
  const comment = queryData?.get("comment");

  if (!isValidUserName(userName)) {
    return {
      state: false,
      message: "Please enter a valid name.",
    };
  }

  if (!isValidAge(Number(age))) {
    return {
      state: false,
      message: "Please enter a valid age.",
    };
  }

  if (!isValidComment(comment)) {
    return {
      state: false,
      message: "Please enter a valid comment.",
    };
  }

  return {
    state: true,
    message: "Form submitted successfully.",
  };
}

ここの部分に注目しましょう

export async function submitForm(
  // @ts-ignore
  state: response | null,
  queryData?: QueryData
): Promise<response> {
// ~~~

引数についてです。

  • state : これはuseActionStateを宣言した時のmessageの値が渡ってきます。この関数の戻り値がmessageの値になるので「一つ前の状態が渡ってくる」と言えばイメージがつきやすいかもしれません。
  • queyData : これはformのinputの値が渡ってきます。注意なのですがinputのnameを必ず指定してください。そうしないとquaryData.get("value")で値を取得できません。
  • // @ts-ignore : これでstateのTypescriptのコンパイル時のチェックを無視してるのですが、正直ここは良い方法がわかりません。TypeScriptは使用しない値に対するチェックが厳しいのですがuseActionStateはstateを必ず渡してくるのでどう対処するのが正解なんでしょう???プロの方教えてください。

今回の関数ではバリデーションチェックをしてstateとmessageをクライアント側に返しています。戻り値によってクライアント側に表示される値を変えています。

ちなみにbuttonタグ等にもformActionを設定できるみたいです。React19で大きく変わったところだとか、それでは〜

Discussion