🎃

React Hook Form触ってみた

2024/03/14に公開1

React Hook Form チュートリアル

自社プロダクトで使用しているReact Hook Formを習得するため触ってみたことのまとめ

今回、社内のエンジニアにReact Hook Formのチュートリアルを用意していただいたのでそれに沿って進める

問題1から10まであり全てまとめると膨大なため重点を一部抜粋している

フォームの要件

フォームの情報

  • 名前(Name)
  • メールアドレス(Email)
  • パスワード(Password)
  • 年齢(Age)
  • 趣味(Hobbies)- 複数選択可能

問題

  1. フォームを初期化し、名前、メールアドレス、パスワード、年齢のデフォルト値を設定してください。
  2. 名前、メールアドレス、パスワードの入力フィールドをフォームに登録してください。
  3. 特定の条件(例:ユーザーが特定のチェックボックスをオンにした場合)で、パスワードフィールドの登録を解除してください。
  4. 各フィールドのバリデーションエラーを表示してください。
  5. メールアドレスの入力値をリアルタイムで監視し、コンソールに表示してください。
  6. フォームの送信を処理し、送信データをコンソールに表示してください。
  7. 送信ボタンの隣にリセットボタンを設置し、フォームの入力値を初期値に戻してください。
  8. ボタンクリックにより、任意のフィールドにエラーを設定し、もう一度クリックでエラーをクリアしてください。
  9. 任意のボタンを用意し、そのボタンをクリックすることでメールアドレスフィールドにプリセットのメールアドレスを設定してください
  10. 趣味(Hobbies)のフィールドを動的に追加・削除できるようにしてください。趣味はテキスト入力で、複数追加可能とします。

1. フォームの初期化

  1. フォームを初期化し、名前、メールアドレス、パスワード、年齢のデフォルト値を設定してください。

useForm

useFormとはReact Hook Formに備わっているフォームを簡単に管理するためのカスタムフック
様々なメソッドが戻り値に用意されている
https://react-hook-form.com/docs/useform

defaultValues

defaultValuesを使うとフォームの初期値を設定できる

useForm({
    defaultValues: {
      name: 'Name',
      email: 'test@gmail.com',
      password: 'passowrd',
      age: '20',
    },
});

register

useFormの戻り値であるregisterメソッドを使用するとinput要素の登録ができる

const { onChange, onBlur, name, ref } = register('name') ;    
<input 
  onChange={onChange}
  onBlur={onBlur}
  name={name}
  ref={ref}
/>

// 以下の様に省略も可能
<input {...register('name')} />

問題1の完成系のコード

import './App.css';
import { useForm, useFieldArray } from 'react-hook-form';

function App() {
  const {
    register,
    handleSubmit,
  } = useForm({
    defaultValues: {
      name: 'Name',
      email: 'test@gmail.com',
      password: 'passowrd',
      age: '20',
    },
  });

  const onSubmit = (data: any) => {
    console.log(data);
  };

  return (
    <>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <div>
            <label htmlFor="">名前</label>
            <input
              {...register('name')}
              type="text"
            />
          </div>
          <div>
            <label htmlFor="">メールアドレス</label>
            <input
              {...register('email')}
              type="email"
            />
          </div>
          <div>
            <label htmlFor="">パスワード</label>
            <input
              {...register('password')}
              type="password"
            />
          </div>
          <div>
            <label htmlFor="">年齢</label>
            <input
              {...register('age')}
              type="number"
            />
          </div>
          <div>
            <button type="submit">送信</button>
          </div>
        </div>
      </form>
    </>
  );
}

export default App;

2. エラーの表示

  1. 各フィールドのバリデーションエラーを表示してください。

formState: { errors }

useFormの戻り値であるformStateにはフォーム全体の状態に関する情報が含まれている
その中のerrorsを使うとバリデーションエラー等を表示できる

registerの第二引数にオブジェクト形式でバリデーション項目とエラーメッセージを設定できる

validateを使うとコールバック関数を引数として渡して検証することも可能

export const FORM_VALIDATE_MESSAGE = {
  required: 'この項目は必須です',
  emailPattern: '正しいメールアドレスを入力してください。',
} as const;

const validateEmail = (value: string) => {
  const re =
    /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
  const isFormatValid = re.test(value);
  const isInvalidDotSyntax =
    /^[.]/.test(value) || /\.{2,}/.test(value) || /\.@/.test(value);
  return isFormatValid && !isInvalidDotSyntax;
};

const {
    register,
    formState: { errors },
  } = useForm({
    defaultValues: {
      name: 'Name',
      email: 'test@gmail.com',
      password: 'passowrd',
      age: '20',
      checkbox: false,
      hobbie: 'test',
    },
  });

<div>
    <label htmlFor="">名前</label>
    <input
      {...register('name', { required: 'Name is required' })}
      type="text"
    />
    {errors.name && <p>{errors.name.message}</p>}
</div>

<div>
    <label htmlFor="">メールアドレス</label>
    <input
      {...register('email', {
        validate: (value) => {
          if (!value) {
            return FORM_VALIDATE_MESSAGE.required;
          }
          if (!validateEmail(value)) {
            return FORM_VALIDATE_MESSAGE.emailPattern;
          }
        },
      })}
      type="email"
    />
    {errors.email && <p>{errors.email.message}</p>}
  </div>

mode: 'onChange'

そのまま使うと送信ボタンを押した時のみエラーが表示される仕様になっている
入力(onChange)中にエラーが表示されるようにしたい場合はmode: 'onChange'を指定すると良い

const {
    register,
    formState: { errors },
  } = useForm({
    defaultValues: {
      name: 'Name',
      email: 'test@gmail.com',
      password: 'passowrd',
      age: '20',
      checkbox: false,
      hobbie: 'test',
    },
    mode: 'onChange',
  });

3. プリセットの設定

  1. 任意のボタンを用意し、そのボタンをクリックすることでメールアドレスフィールドにプリセットのメールアドレスを設定してください

任意のプリセットとボタンを用意する

プリセット

const presetEmail = 'preset@test.com';

ボタン

<button type="button">
  メールアドレスプリセット
</button>

setValue

useFormの戻り値であるsetValueメソッドを使用すると任意の値をフォームに挿入できる

<button
  type="button"
  onClick={() => setValue('email', presetEmail)}
>
  メールアドレスプリセット
</button>

4. フィールドの動的追加・削除

  1. 趣味(Hobbies)のフィールドを動的に追加・削除できるようにしてください。趣味はテキスト入力で、複数追加可能とします。

useFieldArray

useFieldArrayとはフィールド配列 (動的フォーム) を操作するためのカスタムフック
フォームを動的に操作したい場合はuseFormではなくこちらを使う

今回はフォームの追加、削除を行うためappend、remove、fieldsを取り出す

const { append, remove, fields } = useFieldArray();

引数にはフィールドのnameを設定できる。今回は"hobbie"
useFormから取り出したcontrolも渡す。

const { control } = useForm();

const { append, remove, fields } = useFieldArray({
    control,
    name: 'hobbie',
});

フィールド追加ボタンとフィールドを用意する

フィールド追加ボタン

<button type="button">
    +
</button>

フィールド

fieldsにフォームが配列で格納されているのでmapで1つずつ表示させる
registerの引数にフィールド名.${index}.valueを指定する

<div>
    {fields.map((field, index) => (
      <div>
        <label>
          趣味
          <input
            key={field.id}
            {...register(`hobbie.${index}.value`)}
          />
        </label>
      </div>
    ))}
</div>

ボタンのonClick関数を設定する

appendを使うことで、ボタンクリックするごとにhobbieのinputを増やせる

const handleClickAddInput = () => {
    append({ name: 'hobbie' });
};

<button type="button" onClick={() => handleClickAddInput()}>
    +
</button>

削除ボタン実装

ボタンのonClickにremove(index)を指定する
ボタンをクリックするとinputを削除できる

<button type="button" onClick={() => remove(index)}></button>

全体の完成系コード

今回は問題1,4,9,10のみ抜粋したが、全ての問題を含めたコードを以下に記載する

import './App.css';
import { useForm, useFieldArray } from 'react-hook-form';
import { useEffect } from 'react';

export const FORM_VALIDATE_MESSAGE = {
  required: 'この項目は必須です',
  emailPattern: '正しいメールアドレスを入力してください。',
} as const;

const validateEmail = (value: string) => {
  const re =
    /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
  const isFormatValid = re.test(value);
  const isInvalidDotSyntax =
    /^[.]/.test(value) || /\.{2,}/.test(value) || /\.@/.test(value);
  return isFormatValid && !isInvalidDotSyntax;
};

const presetEmail = 'preset@test.com';

function App() {
  const {
    register,
    unregister,
    handleSubmit,
    watch,
    setError,
    clearErrors,
    formState: { errors },
    reset,
    setValue,
    control,
  } = useForm({
    defaultValues: {
      name: 'Name',
      email: 'test@gmail.com',
      password: 'passowrd',
      age: '20',
      checkbox: false,
      hobbie: 'test',
    },
    mode: 'onChange',
  });

  const { append, remove, fields } = useFieldArray({
    control,
    name: 'hobbie',
  });

  const onSubmit = (data: any) => {
    console.log(data);
  };

  const watchCheckbox = watch('checkbox', false);

  const watchEmail = watch('email');

  const handleClickErrorButton = () => {
    if (errors.name) {
      clearErrors();
    } else {
      setError('name', {
        message: 'エラー',
      });
    }
  };

  const handleClickAddInput = () => {
    console.log('input');
    append({ name: 'hobbie' });
  };

  useEffect(() => {
    console.log(watchEmail);
  }, [watchEmail]);

  useEffect(() => {
    if (watchCheckbox) {
      unregister('password');
    }
  }, [watchCheckbox, unregister]);

  return (
    <>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <div>
            <label htmlFor="">名前</label>
            <input
              {...register('name', { required: 'Name is required' })}
              type="text"
            />
            {errors.name && <p>{errors.name.message}</p>}
          </div>
          <div>
            <label htmlFor="">メールアドレス</label>
            <input
              {...register('email', {
                validate: (value) => {
                  if (!value) {
                    return FORM_VALIDATE_MESSAGE.required;
                  }
                  if (!validateEmail(value)) {
                    return FORM_VALIDATE_MESSAGE.emailPattern;
                  }
                },
              })}
              type="email"
            />
            {errors.email && <p>{errors.email.message}</p>}
          </div>
          <div>
            {!watchCheckbox && (
              <>
                <label htmlFor="">パスワード</label>
                <input
                  {...register('password', {
                    required: 'パスワードを入力してください。',
                    minLength: {
                      value: 8,
                      message: 'パスワードは8文字以上で入力してください。',
                    },
                    maxLength: {
                      value: 16,
                      message: 'パスワードは16文字以内で入力してください。',
                    },
                  })}
                  type="password"
                />
                {errors.password && <p>{errors.password.message}</p>}
              </>
            )}
          </div>
          <div>
            <input {...register('checkbox')} type="checkbox" />
          </div>
          <div>
            <label htmlFor="">年齢</label>
            <input
              {...register('age', { required: 'age is required' })}
              type="number"
            />
            {errors.age && <p>{errors.age.message}</p>}
          </div>
          <div>
            <button type="submit">送信</button>
            <button type="button" onClick={() => reset()}>
              リセット
            </button>
            <button type="button" onClick={() => handleClickErrorButton()}>
              {errors.name ? 'エラーリセット' : 'エラー設定'}
            </button>
            <button
              type="button"
              onClick={() => setValue('email', presetEmail)}
            >
              メールアドレスプリセット
            </button>
          </div>
        </div>
        <div>
          <button type="button" onClick={() => handleClickAddInput()}>
            +
          </button>
          <div>
            {fields.map((field, index) => (
              <div>
                <label>
                  趣味
                  <input
                    key={field.id} // important to include key with field's id
                    {...register(`hobbie.${index}.value`)}
                  />
                </label>
                <button type="button" onClick={() => remove(index)}></button>
              </div>
            ))}
          </div>
        </div>
      </form>
    </>
  );
}

export default App;

Discussion

nap5nap5

今回、社内のエンジニアにReact Hook Formのチュートリアルを用意していただいたのでそれに沿って進める
問題1から10まであり全てまとめると膨大なため重点を一部抜粋している

ContextのProviderでグローバルな読宣の機能を与えるとともに、Flag設定の更新はRHFで管轄するといった形になります

問題にあるかは不明ですが、Feature Flagのユースケースもバリエーションに加えると、いいかもです

demo code.

https://codesandbox.io/p/sandbox/test-app-57xxgj?file=%2Fsrc%2FApp.tsx