技術っぽいことを書いてみるブログ

PythonとかVue.jsとか技術的なことについて書いていきます。

Reactについて学ぶ~フォームをバインドする編~

はじめに

今回は、Reactでフォームデータをバインドする方法についてまとめておきます。

Reactでフォームデータを扱うときのポイントは、

  • コンポーネントからのイベントは、 React が提供する Synthetic Event が適用される
  • フォームの値をコンポーネントの state として持ち、値の反映には state のリフトアップを使う

らしいです。

フォームデータを扱う(その1)

  • サンプルコード
import { ChangeEvent, FC, useState } from 'react';

const divisionCode = { 1: '区分1', 2: '区分2', 3: '区分3' } as const;

type FormData = {
  name: string;
  age?: number;
  division: keyof typeof divisionCode;
};

const AbouutForm: FC = () => {
  const [formData, setFormData] = useState<FormData>({
    name: '',
    age: undefined,
    division: 1,
  });

  const changeHandler = (
    event: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
  ) => {
    const { name } = event.target;
    const value = event.target.value;
    setFormData((f) => ({ ...f, [name]: value }));
  };

  const clickButtonHandler = () => {
    console.log(formData);
  };

  return (
    <div
      style={{
        marginTop: '50px',
        textAlign: 'left',
        borderWidth: '2px',
        borderColor: '#ff0000',
        borderStyle: 'solid',
      }}
    >
      <div>
        <label>なまえ</label>
        <input
          name="name"
          type="text"
          value={formData.name}
          onChange={changeHandler}
        />
      </div>

      <div>
        <label>年齢</label>
        <input
          type="number"
          name="age"
          value={formData.age}
          onChange={changeHandler}
        />
      </div>

      <div>
        <label>区分</label>
        <select
          name="division"
          placeholder="区分を選択…"
          value={formData.division}
          onChange={changeHandler}
        >
          {Object.entries(divisionCode).map(([code, name]) => (
            <option value={code} key={code}>
              {name}
            </option>
          ))}
        </select>
      </div>

      <div>
        <button onClick={clickButtonHandler}>テスト</button>
      </div>
    </div>
  );
};

export default AbouutForm;

解説

制御されたコンポーネント

  • 画面のフォームデータは、formDataとしてstateで持ち回っています。
    • stateで管理されるフォーム要素のことを制御されたコンポーネントというらしい。。。
    • Reactでは、フォームの実装は、この制御されたコンポーネントを使うことが推奨となっているみたいです。
    • 逆にrefを使用して、リアルDOMを参照し、直接フォーム要素の値にアクセスすることを非制御コンポーネントという
      • 特段理由がなければ、制御されたコンポーネントで実装する方がよさそうです。

Synthetic Event について

  • Reactは、単方向バインディングになっているので、value={formData.name}とすることで、初期値がセットされます。
  • 単方向なので、画面上で入力された値をformDataのstateに反映させる必要があります。
    • onClickやonChange属性のコールバック関数の引数のイベントが、React独自のSyntheticEventオブジェクトになります。
    • inputタグのonChange属性であれば、SyntheticEventオブジェクトから拡張されたChangeEventを適用する必要がある。
    • なので、onChagen属性で指定しているコールバック関数の引数の型は、ChangeEvent<T>となっています。
      • Tは、HTMLInputElementなどのHTML要素の型を指定します。
    • このonChange属性のコールバック関数で、stateを更新し、画面入力との同期を図ります。
  • 上記のサンプルコードでは、inputのonChangeでchangeHandler関数を呼び出し、changeEventから入力された値を取得し、stateに反映しています。
    • なお、inputのname属性で、stateの値を更新するので、stateのプロパティ名とinputのname属性は揃えておく必要があります。

フォームデータを扱う(その2:フォームヘルパーを使用する)

フォームヘルパーには何がある?

Reactのフォームヘルパーは、formikreact-hook-formなどいろいろあるみたいですが、
npm trendsを見る限り、主流はreact-hook-formのようです。

React Hook Formを使う

  • インストール
npm i react-hook-form

React Hook Formを使った実装

  • サンプルコード
import { FC } from 'react';
import { useForm } from 'react-hook-form';

const divisionCode = { 1: '区分1', 2: '区分2', 3: '区分3' } as const;

type FormData = {
  name: string;
  age?: number;
  division: keyof typeof divisionCode;
};

const AbouutForm2: FC = () => {
  const { register, reset, getValues } = useForm<FormData>({
    defaultValues: {
      name: '',
      division: 1,
    },
  });

  const resetHandler = () => {
    console.log('reset');
    reset();
  };

  const clickButtonHandler = () => {
    console.log('clickButtonHandler');
    console.log(getValues());
  };

  return (
    <div
      style={{
        marginTop: '50px',
        textAlign: 'left',
        borderWidth: '2px',
        borderColor: '#ff0000',
        borderStyle: 'solid',
      }}
    >
      <div>
        <label>なまえ</label>
        <input {...register('name')} />
      </div>

      <div>
        <label>年齢</label>
        <input type="number" {...register('age')} />
      </div>

      <div>
        <label>区分</label>
        <select placeholder="区分を選択…" {...register('division')}>
          {Object.entries(divisionCode).map(([code, name]) => (
            <option value={code} key={code}>
              {name}
            </option>
          ))}
        </select>
      </div>

      <div>
        <button onClick={clickButtonHandler}>テスト</button>
        <button onClick={resetHandler}>リセット</button>
      </div>
    </div>
  );
};

export default AbouutForm2;

解説

useForm

  • useForm<T>のTでstateする型を指定する
  • useFormの引数は、様々な指定ができる
    • defaultValues: フォーム要素のデフォルト値を設定する
    • 詳細は、こちらを参照にするとよい。
  • useFormの戻り値も、様々なものがオブジェクトとして含まれている。
    • 必要となるもののみ分割代入で使用するようにするとよい
      • 詳細は、こちらを参照にするとよい。
    • register: フォーム要素をReactHookFormの管理下に置くように登録するもの
      • {...register('name')}をログ出力すると、{name: 'name', onChange: ƒ, onBlur: ƒ, ref: ƒ}のように出力されます。
        • つまり、フォーム要素で、<input {...register('name')} />のように使うことで、name属性、onChange属性、onBlur属性を設定してくれます。
        • これは、フォームデータを扱う(その1)の実装に比べてだいぶ楽になります。

getValues

  • useFormで生成されたgetValues関数を実行することで、フォームデータを取得できます。

reset

  • useFormで生成されたreset関数を実行することで、フォームデータをdefalutValuesに戻してくれます。

フォームデータを扱う(その3:バリデーション)

yupを使う

  • インストール
npm i yup @hookform/resolvers

yupを使った実装

  • サンプルコード
import { FC } from 'react';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';
import type { InferType } from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import FormErrorMessage from './FormErrorMessage';
import type { SubmitHandler } from 'react-hook-form';

const divisionCode = { 1: '区分1', 2: '区分2', 3: '区分3' } as const;

type FormData = {
  name: string;
  age?: number;
  division: number;
};

yup.setLocale({
  mixed: {
    notType: '不正な値が設定されています',
  },
});

const regFormSchema = yup.object({
  name: yup
    .string()
    .max(10, '10文字以下で入力してね🎵')
    .required('必須項目です'),
  age: yup.number().max(120, '120歳以下しか受け付けません!'),
  division: yup.number().required('必須項目です'),
});

type RegFormSchema = InferType<typeof regFormSchema>;

const AbouutForm2: FC = () => {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<FormData>({
    defaultValues: {
      name: '',
      division: 1,
    },
    resolver: yupResolver(regFormSchema),
  });

  const resetHandler = () => {
    console.log('reset');
    reset();
  };

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

  return (
    <div
      style={{
        marginTop: '50px',
        textAlign: 'left',
        borderWidth: '2px',
        borderColor: '#ff0000',
        borderStyle: 'solid',
      }}
    >
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label>なまえ</label>
          <input {...register('name')} />
          <FormErrorMessage>{errors.name?.message}</FormErrorMessage>
        </div>

        <div>
          <label>年齢</label>
          <input type="number" {...register('age')} />
          <FormErrorMessage>{errors.age?.message}</FormErrorMessage>
        </div>

        <div>
          <label>区分</label>
          <select placeholder="区分を選択…" {...register('division')}>
            {Object.entries(divisionCode).map(([code, name]) => (
              <option value={code} key={code}>
                {name}
              </option>
            ))}
          </select>
          <FormErrorMessage>{errors.division?.message}</FormErrorMessage>
        </div>

        <div>
          <button type="submit">送信</button>
          <button onClick={resetHandler}>リセット</button>
        </div>
      </form>
    </div>
  );
};

export default AbouutForm2;

解説

yup.object

  • yup.objectで入力フォームのバリデーションの設定を行います。
    • なお、number型のチェックにおいて、ブランク時のチェックでmust be anumbertype, but the final value was: NaN (cast from the value "")みたいなエラーメッセージになるので yup.setLocaleでメッセージを日本語化しておきます。
      • このあたりの共通化は課題ですね・・・。。。

yupResolver(regFormSchema)

  • yup.objectでバリデーションの生成ができたら、useFormのresolverに渡します。
  • また、バリデーションエラーの情報を使うために、useFormのformStateからerrorsを使用しています。
    • このerrorsから画面にエラーメッセージを表示させています。

課題

  • 上記のサンプルでは、「リセット」ボタンを押下したときに、エラーメッセージがクリアされないです。
    • そこも含めてリセットしたいのですが、今のことろ策なしです…。

最後に・・・、所感

  • 今回は、Reactでフォームデータの取り扱いを学びました。
  • yupの使い方には、まだまだ研究が必要そうで、実際の業務で使うとなると、もっと課題になりそう。。。
  • React Hook Formは、使わないケースに比べて、より簡潔に書けるので、使わない手はなさそうです。
  • まぁ、触ってみることで課題というか、疑問がわいてくるので、そのあたりは良いことかなと思いますね。