logo

useActionStateで少し実装しやすくなった

2024-06-13
6 months ago

開発環境

  • next 15.0.0-canary.20
  • react 19.0.0-rc-f994737d14-20240522
  • babel-plugin-react-compiler 0.0.0-experimental-938cd9a-20240601
  • @conform-to/react 1.1.4

前提

Reactのバージョンは19を想定しています。

実装例の中でフォームバリデーションライブラリのconformを利用していますが、本題とはズレますので特に解説などは行なっておりませんのでご了承ください。

本題

React 19以前ではuseFormStateからフォームの送信状態を参照することができなかったので、useFormStatusを利用したいました。

実装例

'use client';

import { useForm, getFormProps } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { useFormState, useFormStatus } from 'react-dom';

import { Field, FieldErrors } from '@/components/form/field';
import { FormInput } from '@/components/form/form-input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';

import { login } from '../actions/login';
import { loginSchema } from '../schemas/login';

const SubmitButton = () => {
  const { pending } = useFormStatus();

  return (
    <Button type="submit" form="login" disabled={pending} className="w-full">
      ログイン
    </Button>
  );
};

export const LoginForm = () => {
  const [lastResult, action] = useFormState(login, undefined);
  const [form, fields] = useForm({
    id: 'login',
    lastResult,
    onValidate({ formData }) {
      return parseWithZod(formData, { schema: loginSchema });
    },
  });

  return (
    <form
      className="grid gap-4"
      {...getFormProps(form)}
      action={action}
      noValidate
    >
      <Field>
        <Label htmlFor={fields.email.id}>メールアドレス</Label>
        <FormInput meta={fields.email} type="text" />
        <FieldErrors errors={fields.email.errors} />
      </Field>
      <Field>
        <Label htmlFor={fields.password.id}>パスワード</Label>
        <FormInput meta={fields.password} type="password" />
        <FieldErrors errors={fields.password.errors} />
      </Field>
      <SubmitButton />
      <FieldErrors errors={form.errors} />
    </form>
  );
};

このuseFormStatus<form>の中にレンダリングされるコンポーネントで使用することができるという条件があったので、実装例のようにコンポーネントを分けて実装していました。

React 19で新たに追加されたuseActionStateを利用すると分ける必要がなくなります。

※今後はuseFormStateは非推奨になります。

useActionStateで実装例を書き換える

'use client';

import { useForm, getFormProps } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
// import { useFormState, useFormStatus } from 'react-dom';

import { Field, FieldErrors } from '@/components/form/field';
import { FormInput } from '@/components/form/form-input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';

import { login } from '../actions/login';
import { loginSchema } from '../schemas/login';
import { useActionState } from 'react';

// const SubmitButton = () => {
//   const { pending } = useFormStatus();

//   return (
//     <Button type="submit" form="login" disabled={pending} className="w-full">
//       ログイン
//     </Button>
//   );
// };

export const LoginForm = () => {
  const [lastResult, action, pending] = useActionState(login, undefined);
  const [form, fields] = useForm({
    id: 'login',
    lastResult,
    onValidate({ formData }) {
      return parseWithZod(formData, { schema: loginSchema });
    },
  });

  return (
    <form
      className="grid gap-4"
      {...getFormProps(form)}
      action={action}
      noValidate
    >
      <Field>
        <Label htmlFor={fields.email.id}>メールアドレス</Label>
        <FormInput meta={fields.email} type="text" />
        <FieldErrors errors={fields.email.errors} />
      </Field>
      <Field>
        <Label htmlFor={fields.password.id}>パスワード</Label>
        <FormInput meta={fields.password} type="password" />
        <FieldErrors errors={fields.password.errors} />
      </Field>
      {/* <SubmitButton /> */}
      <Button type="submit" form="login" disabled={pending} className="w-full">
        ログイン
      </Button>
      <FieldErrors errors={form.errors} />
    </form>
  );
};

さいごに

useFormStatusは非推奨になるわけではないので、ケースバイケースで利用していくことになります。

また、今回の本題とはズレてしまうので言及していませんでしたが、筆者が最近推しているconformuseActionStateでそのまま利用することができました。(簡単な動作確認しかしていないですが)

参照