2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[React] Formの取り扱いについて理解する!

Last updated at Posted at 2024-05-17

Reactでのフォームの取り扱い

Reactでのフォームの取り扱いについて、要点をまとめました。


フォームデータの取り扱い

Reactでのフォームデータの取扱いは、大きく分けて2パターンあリます。
それぞれの違いを見ていきましょう。

「制御コンポーネント」 を用いる方法

フォームの値を親コンポーネントでstateとして管理し、値の反映には stateのリフトアップ を用いる。

stateのリフトアップが理解できていない方はこちらの記事をご確認ください。

  • メリット
    • 常に値にアクセスできるので、ユーザーの入力中にバリデーションが可能
  • デメリット
    • 入力値が更新されるたびにコンポーネントが再レンダリングされる

Reactは、親から子の単方向データフロー を採用しているため、基本的には制御コンポーネントを用いる方法が推奨されています。

「非制御コンポーネント」 を用いる方法

仮想DOMを経由せず直接DOM要素の値にアクセスする手法
useRefを使用する

  • メリット
    • 入力値が更新されるたびにコンポーネントが再レンダリングされないので、パフォーマンス効率が良い
  • デメリット
    • ユーザーの入力中にバリデーションをすることが難しい

イベントパラメータについて

Reactでは、コンポーネントでイベントを扱う際、Eventオブジェクトではなく、React独自の Synthetic Event を用います。

イベントパラメータを引数に受け取る場合は、SyntheticEventを継承した、特定のイベント型を指定する必要があります。

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setFormData({ ...formData, inputValue: event.target.value });
};
// ...
<input type="text" onChange={handleChange} />

:pencil: イベントハンドラと対応するデータ型

type Props = {
  onClick: (event: React.MouseEvent<HTMLInputElement>) => void
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
  onkeypress: (event: React.KeyboardEvent<HTMLInputElement>) => void
  onBlur: (event: React.FocusEvent<HTMLInputElement>) => void
  onFocus: (event: React.FocusEvent<HTMLInputElement>) => void
  onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
  onCancel: (event: React.React.SyntheticEvent<HTMLDialogElement>) => void
}

[サンプルコード] 制御コンポーネント

ここまで基本の理解はできたかと思います。
制御コンポーネントのサンプルコードを見ていきましょう。

前提

  • フォーム全体のデータを親コンポーネントでstate管理
  • フォームデータを更新するためのコールバック関数を定義し、子コンポーネントのイベントとして設定
import React, { useState } from 'react';

type FormData = {
  firstName: string;
  lastName: string;
};

const ControlledComponent: React.FC = () => {
  const [formData, setFormData] = useState<FormData>({
    firstName: '',
    lastName: '',
  });

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData((prevData) => ({
      ...prevData,
      [name]: value,
    }));
  };

  return (
    <form>
      <input
        type="text"
        name="firstName"
        value={formData.firstName}
        onChange={handleChange}
      />
      <input
        type="text"
        name="lastName"
        value={formData.lastName}
        onChange={handleChange}
      />
    </form>
  );
};

:pencil: [サンプルコード] 非制御コンポーネント

  • useRefを使用して、コンポーネントの参照を保持(仮想DOMを介さない)
  • 値の取得 / 更新時はDOM要素を直接参照 / 更新する
import React, { useRef } from 'react';

const UncontrolledComponent: React.FC = () => {
  const firstNameRef = useRef<HTMLInputElement>(null);
  const lastNameRef = useRef<HTMLInputElement>(null);

  return (
    <form>
      <input type="text" name="firstName" ref={firstNameRef} />
      <input type="text" name="lastName" ref={lastNameRef} />
    </form>
  );
};

[:pencil: 応用] React Hook Form

Reactにはフォームデータを扱うためのライブラリがいくつか用意されています。
デファクト・スタンダードとされているものはなく、代表的なものは以下の通りです。

  • React Hook Form
  • Formik
  • React Final Form

npm trends はこのような感じ(20240810時点)
image.png

React Hook Formとは?

フォームヘルパーの1つ。
複雑なフォームを扱う場合などに用いられる。
特徴として、非制御コンポーネントを用いて、フォームの入力データを管理します。

サンプルコード

React Hook Formを利用するまでの基本的な流れです。
1.ㅤuseFormをインポート
2.ㅤuseFormの引数にオプションを指定、戻り値で利用する機能をオブジェクトプロパティとして受け取る

const { register, handleSubmit, reset } = useForm<FormData>({
    defaultValues: { username: '', isAgreed: false, },
});

3.ㅤregister関数でフォーム要素を登録する

<input  {...register('username')} />
  • 引数で指定した値(今回の場合はusername)がセットされた、name属性が付与
  • 戻り値にrefプロパティが含まれるため、非制御コンポーネントによるDOM管理が実施

4.ㅤhandleSubmitでのサブミット処理のハンドリング

const onSubmit: SubmitHandler<UserForm> = (data) => {
    console.log(data);
};

//...
<form action="#" onSubmit={handleSubmit(onSubmit)}>

バリデーション

react-hook-formにはバリデーションをサポートするカスタムリゾルバが複数ある。

カスタムリゾルバとは?

フォームバリデーションをカスタマイズするために使用。
独自のバリデーションロジックを実装したい場合に用いられる。

yup

カスタムリゾルバの1つ
旬が過ぎたかもしれないが...yupの記事が多かったのでyupを使用します。

image.png

サンプルコード

yupを利用するまでの基本的な流れです。

1.ㅤsrc/schema にフォームデータのスキーマを定義したファイルの作成

src/schema/userForm.ts

import * as yup from 'yup';
import type { InferType } from 'yup';
import { sex, userRole } from '@/constants/user_form_constants';

export const userForm = yup.object({
  name: yup.string().required('名前の入力は必須です'),
  email: yup.string().required('メールアドレスの入力は必須です'),
  sex: yup.mixed().oneOf(Object.keys(sex)),
  role: yup.mixed().oneOf(Object.keys(userRole)),
  agreement: yup.boolean().oneOf([true], '利用規約の同意が必要です').required(),
});

export type UserFormSchema = InferType<typeof userForm>;

2.ㅤFormコンポーネントにインポート

import { yupResolver } from '@hookform/resolvers/yup';
import type { UserFormSchema } from '@/schema/userForm';
import { userForm } from '@/schema/userForm';

3.ㅤuseFormの変更

  • ジェネリックにインポートしたスキーマのデータ型を指定
  • 第二引数にyupResolverを指定
  • 戻り値からformState: { errors } の取得
const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<UserFormSchema>({
    defaultValues: {
      name: '',
      email: '',
      sex: 'man',
      role: 'readonly',
      agreement: false,
    },
    resolver: yupResolver(userForm),
  });

4.ㅤエラーメッセージの表示

  • {errors.username?.message}

最終的なコード
import { yupResolver } from '@hookform/resolvers/yup';
import { SubmitHandler, useForm } from 'react-hook-form';

import type { UserFormSchema } from '@/schema/userForm';

import { sex, userRole } from '@/constants/user_form_constants';
import { userForm } from '@/schema/userForm';

const SampleReactHookYupResolver = () => {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<UserFormSchema>({
    defaultValues: {
      name: '',
      email: '',
      sex: 'man',
      role: 'readonly',
      agreement: false,
    },
    resolver: yupResolver(userForm),
  });

  const onReset = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();
    reset();
  };

  const onSubmit: SubmitHandler<UserFormSchema> = (data) => {
    console.log(data);
  };

  return (
    <>
      <form action="#" onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label htmlFor="name">氏名</label>
          <input id="name" {...register('name')} />
          <div>{errors.name?.message}</div>
        </div>
        <div>
          <label htmlFor="email">メールアドレス</label>
          <input id="email" {...register('email')} />
          <div>{errors.email?.message}</div>
        </div>
        <div>
          <label htmlFor="sex">性別</label>
          <select {...register('sex')}>
            {Object.entries(sex).map(([key, value], index) => (
              <option key={index} value={key}>
                {value}
              </option>
            ))}
          </select>
          <div>{errors.sex?.message}</div>
        </div>
        <div>
          <label htmlFor="role">ロール</label>
          <select id="role" {...register('role')}>
            {Object.entries(userRole).map(([key, value], index) => (
              <option key={index} value={key}>
                {value}
              </option>
            ))}
          </select>
          <div>{errors.role?.message}</div>
        </div>
        <div>
          <label htmlFor="agreement">利用規約</label>
          <input id="agreement" type="checkbox" {...register('agreement')} />
          <div>{errors.agreement?.message}</div>
        </div>
        <div>
          <button>Submit</button>
        </div>
        <div>
          <button onClick={onReset}>Reset</button>
        </div>
      </form>
    </>
  );
};

export default SampleReactHookYupResolver;

まとめ

以上です。
覚えることが多いな! って印象でした。
ユーザビリティが高いフォームを作っている皆さんに感謝😇

2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?