🔮

React 19 Actionで変わるReactのフォーム

2024/09/01に公開

React19が発表されてから数ヶ月が経ちました。その目玉でもあるActionは今までのReactのフォームを一新するような内容でした。フォーム実装で欠かせなかったReact Hook Formを使わずに実装することも増えていくのでないでしょうか。本記事ではReact Hook FormとReact19のActionを比較していこうと思います。

サンプルコード

比較に際して、React19 RCのサンプルコードを一部改変したものを使用します。

React19
// Using <form> Actions and useActionState
function ChangeName({ name, setName }) {
  const onSubmit = async (previousState, formData) => {
    const error = await updateName(formData.get("name"));
    if (error) {
       return error;
    }
    redirect("/path");
    return null;
  };

  const [
    error,
    submitAction,
    isPending,
  ] = useActionState(onSubmit, null);

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>Update</button>
      {error && <p>{error}</p>}
    </form>
  );
}

下記コードはReact19のサンプルコードをReact Hook Formに修正したものです。

React Hook Form
function ChangeName({ name, setName }) {
  const {
    register,
    handleSubmit,
    setError,
    formState: { isSubmitting, errors },
  } = useForm();

  const onSubmit = async (formData) => {
    const error = await updateName(formData.name);
    if (error) {
      setError("root.serverError", {
        type: "400",
        message: error,
      });
      return null;
    }
    redirect("/path");
    return null;
  };

  return (
    <form action={handleSubmit(onSubmit)}>
      <input type="text" {...register("name")} />
      <button type="submit" disabled={isSubmitting}>Update</button>
      {errors.root.serverError.message && <p>{errors.root.serverError.message}</p>}
    </form>
  );
}

useActionStateとuseForm

React19(サンプルコードから一部抜粋)
// Using <form> Actions and useActionState
function ChangeName({ name, setName }) {
  const onSubmit = async (previousState, formData) => {
    // ...
    if (error) {
       return error;
    }
    // ...
    return null;
  };

  const [
    error,
    submitAction,
    isPending,
  ] = useActionState(onSubmit, null);

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>Update</button>
      {error && <p>{error}</p>}
    </form>
  );
}
React Hook Form(サンプルコードから一部抜粋)
function ChangeName({ name, setName }) {
  const {
    register,
    handleSubmit,
    setError,
    formState: { isSubmitting, errors },
  } = useForm();

  const onSubmit = async (formData) => {
    // ...
  };

  return (
    <form action={handleSubmit(onSubmit)}>
      <input type="text" {...register("name")} />
      <button type="submit" disabled={isSubmitting}>Update</button>
      {errors.root.serverError.message && <p>{errors.root.serverError.message}</p>}
    </form>
  );
}

useActionStateは公式ドキュメントで下記のように紹介されています。本記事では以下の留意点から、公式ドキュメントから命名を変えて紹介します。

- const [state, formAction] = useActionState(fn, initialState, permalink?);
+ const [actionState, submitAction, isPending] = useActionState(fn, initialState, permalink?);
  • stateという命名がuseStateと衝突するため、actionStateとする。
  • formActionという命名が<form>でしか使えないという誤解を与えてしまうかもしれないので、submitActionとする。
  • isPendingはReferenceに記載がないが、RCでは記載されている。コードを確認したところ、機能自体はできていないがPRは出ていたので、RCでの紹介通りに実装されているとする。
  1. actionState

    現在の state。初回レンダー時には、渡した initialState と等しくなります。アクションが呼び出された後は、そのアクションが返した値と等しくなります。

    これはReact Hook FormのformStateと同様の役割だと考えています。formStateのうち、isSubmitted, isSubmitSuccessful, submitCount, errorsは、actionStateでも再現できそうです。

  2. submitAction

    フォームコンポーネントの action プロパティや、フォーム内の任意の button コンポーネントの formAction プロパティとして渡すことができる新しいアクション。

    React Hook FormのhandleSubmitに相当するものです。handleSubmitと使い方が少し違うので、後ほど詳しく紹介します。

  3. isPending

    the pending state of the action and any state updates contained

    pendingに関してはPromiseでよく出てくるので、説明は不要でしょう。React Hook FormでいうところのformState.isSubmittingです。

submitActionとhandleSubmit

React19(サンプルコードから一部抜粋)
// Using <form> Actions and useActionState
function ChangeName({ name, setName }) {
  const onSubmit = async (previousState, formData) => {
    const error = await updateName(formData.get("name"));
    if (error) {
       return error;
    }
    redirect("/path");
    return null;
  };

  const [
    error,
    submitAction,
    ...,
  ] = useActionState(onSubmit, null);

  return (
    <form action={submitAction}>
      {/* ... */}
    </form>
  );
}
React Hook Form(サンプルコードから一部抜粋)
function ChangeName({ name, setName }) {
  const {
    handleSubmit,
    ...,
  } = useForm();

  const onSubmit = async (formData) => {
    const error = await updateName(formData.name);
    if (error) {
      setError("root.serverError", {
        type: "400",
        message: error,
      });
      return null;
    }
    redirect("/path");
    return null;
  };

  return (
    <form action={handleSubmit(onSubmit)}>
      {/* ... */}
    </form>
  );
}

submitActionは公式ドキュメントで下記のように紹介されています。

フォームコンポーネントの action プロパティや、フォーム内の任意の button コンポーネントの formAction プロパティとして渡すことができる新しいアクション。

つまり、submitActionはhandleSubmitと同様のフォーム送信用の関数です。しかし、実行する関数の渡し方に違いがあります。React19ではuseActionStateの第一引数に渡すのに対して、React Hook FormではhandleSubmitの第一引数に渡します。

また、渡す関数も異なっています。下記は
useActionStateの第一引数の紹介です。

fn: フォームが送信されたりボタンが押されたりしたときに呼び出される関数。この関数が呼び出される際には、1 番目の引数としてはフォームの前回 state(初回は渡した initialState、2 回目以降は前回の返り値)を受け取り、次の引数としてはフォームアクションが通常受け取る引数を受け取ります。

つまり、submitActionの場合はactionStateと実行関数を引数に取り、新しいactionStateを返します。一方で、handleSubmitの場合は実行関数のみを引数に取り、返り値にルールはありません。

あとがき

本記事ではReact19 RCからわかるReact19とReact Hook Formの違いを紹介しました。実際にReact19がリリースされた際に仕様が違う可能性も十分に考えられます。その点も踏まえた上で、useActionStateに触れる方のご参考になればと思います。

https://ja.react.dev/blog/2024/04/25/react-19

https://ja.react.dev/reference/react/useActionState

https://github.com/facebook/react/pull/28491

https://react-hook-form.com/docs/useform/formstate

https://react-hook-form.com/docs/useform/handlesubmit

Discussion