見出し画像

TypeScriptでreact-hook-formを使ってみた

Reactで独自にフォームを実装しようとすると、CustomHooksに大量のロジックが必要になり、コード量もかなりの量になることがあります。
多くの人がこの面倒な部分に遭遇しているかと思います。
そのため、react-hook-formFormikなどの有名なライブラリが利用されることがあります。
今回はreact-hook-formを使用してみましたが、ハマったポイントなどをここにまとめておきたいと思います。

まず、パッケージをインストールしましょう。

$ yarn add -D react-hook-form

では、ざっくりとした今回のコードを紹介します。
UI部分はMaterial UIを使用しています。

import React, { ReactElement } from 'react';
import { useForm } from 'react-hook-form';
import { Box, Grid, makeStyles, InputBase } from '@material-ui/core';

export const Form = (): ReactElement => {
 const { handleSubmit, errors, register, formState, reset } = useForm<IFormDatas>({
  mode: 'onChange',
  reValidateMode: 'onBlur',
});

const { isDirty, isValid } = formState;
const [_isChecked, _setChecked] = useState<boolean>(false);

/**
 * フォームをサブミットするときのコールバック
 */
const onSubmitHandler = () => {
  console.log('onSubmit!')
}

/**
 * チェックボックスのON/OFFを行うハンドラー
 */
const onCheckHandler = (): void => {
  _setChecked(!_isChecked);
};

<form onSubmit={handleSubmit(onSubmitHandler)}>

    // ----------------------------------------
    // firstName
    // ----------------------------------------
    <Box mt="30px">
      <FormLabel label="firstName" />
        <InputBase
          name="firstName"
          inputRef={register(errorMessages.firstName)}
          className={`${classes.field} ${errors.firstName ? classes.errorField : ''}`}
          fullWidth
          placeholder="YAMADA"
        />
        {errors.firstName && (
          <Box>
            <FormError errorField={errors.firstName} message={errors.firstName.message} />
          </Box>
        )}
    </Box>

    // ----------------------------------------
    // lastName
    // ----------------------------------------
    <Box mt="30px">
       <FormLabel label="lastName" />
         <InputBase
           name="lastName"
           inputRef={register(errorMessages.lastName)}
           className={`${classes.field} ${errors.lastName ? classes.errorField : ''}`}
           fullWidth
           placeholder="TARO"
         />
         {errors.lastName && (
           <Box>
             <FormError errorField={errors.lastName} message={errors.lastName.message} />
           </Box>
         )}
      </Box>

    // ----------------------------------------
    // email
    // ----------------------------------------
    <Box mt="30px">
      <FormHeading label="email" required />
      <InputBase
        name="email"
        inputRef={register(errorMessages.email)}
        className={`${classes.field} ${errors.email ? classes.errorField : ''}`}
        fullWidth
      />
      {errors.email && (
        <Box>
          <FormError errorField={errors.email} message={errors.email.message} />
        </Box>
      )}
    </Box>

    // ----------------------------------------
    // inquiry
    // ----------------------------------------
    <Box mt="30px">
      <FormHeading label="inquiry" required />
      <InputBase
        name="body"
        inputRef={register(errorMessages.body)}
        className={`${classes.field} ${errors.body ? classes.errorField : ''}`}
        rows={6}
        rowsMax={6}
        fullWidth
        multiline
      />
      {errors.body && (
        <Box>
          <FormError errorField={errors.body} message={errors.body.message} />
        </Box>
      )}
    </Box>

    // ----------------------------------------
    // checkbox
    // ----------------------------------------
    <Box>
      <label htmlFor="agree" className={classes.label}>
        <input
          name="agree"
          ref={register(errorMessages.agree)}
          type="checkbox"
          className={classes.check}
        />
        <span className={classes.labelText}>個人情報のお取り扱いに同意する</span>
      </label>
      {errors.agree && (
        <Box>
          <FormError errorField={errors.agree} message={errors.agree.message} />
        </Box>
      )}
    </Box>

    // ----------------------------------------
    // submit
    // ----------------------------------------
    <Box mt="48px" display="flex" justifyContent="center" alignItems="center">
      <Button
        color="primary"
        type="submit"
        disabled={!(isDirty && isValid)}
      />
       送信する
      </Button>
    </Box>
  </form>

今回のフォームの要素は以下の通りです。

● 姓名
● メールアドレス
● お問い合わせ内容
● チェックボックス
● 送信ボタン

まず、Hooksの部分を見ていきましょう。

const { handleSubmit, errors, register, formState, reset } = useForm<IFormDatas>({
  mode: 'onChange',
  reValidateMode: 'onBlur',
});

const { isDirty, isValid } = formState;
const [_isChecked, _setChecked] = useState<boolean>(false);

まず、行っているのはuseFormを初期化することです。
ここではいくつかのオブジェクトを分割代入しています。

● HandleSubmit
formタグにonSubmit={handleSubmit(formed.onSubmitHandler)}のようにhandleSubmitにコールバックを渡して使用します。

● errors
erorrsオブジェクトには後ほど出てくるバリデーション時のerrorが格納されます。
このオブジェクトを用いてエラーハンドリングが可能です。

● register
バリデーションを設定することができます。
registerを追加する際に設定することが可能です。
※通常のエレメントにはRefですがMaterial UIのエレメントにはinputRefで指定しなければ動かないので注意。

● formState
isDirtyとisValidを取り出すために使用しています。
isDirtyはフォームが初期値のままだとTrueに、isValidはエラーがひとつでもある場合にTrueになります。
主にエラーがゼロになった段階で送信ボタンを押せるようになるなどのアクションのハンドリングをするためのものになります。

バリデーションの一覧はこちら
https://react-hook-form.com/get-started/#Applyvalidation

初期化時に、useFormにジェネリクスとオブジェクトを設定して初期化しています。

// エラーオブジェクトの型をジェネリクスで指定するとerrors.firstNameのように型補完がきく

export type IFormDatas = {
    firstName: string;
    lastName: string;
    email: string;
    agree: string;
    body: string;
};

useForm<IFormDatas>({
    mode: 'onChange',
    reValidateMode: 'onBlur',
});

modeやreValidateModeは、こちらの説明にもあるように、バリデーションを実行するタイミングを指定できるオプションです。
では、次に実際のテキストフィールドを見ていきましょう。

// ----------------------------------------
// firstName
// -----------------------------------------
<Box mt="30px">
  <FormLabel label="firstName" />
    <InputBase
      name="firstName"
      inputRef={register(errorMessages.firstName)}
      className={`${classes.field} ${errors.firstName ? classes.errorField : ''}`}
      fullWidth
      placeholder="YAMADA"
    />
    {errors.firstName && (
      <Box>
        <FormError errorField={errors.firstName} message={errors.firstName.message} />
      </Box>
    )}
</Box>

ここでは、Material UIのInputBaseコンポーネントを使用しています。
なぜTextFieldコンポーネントではなくInputBaseを使用しているかというと、単純にスタイルをカスタマイズしやすいからです。

そして、ここでのポイントはregisterです。

inputRef={register(errorMessages.firstName)}

先程の説明の通り、registerではバリデーションの条件を指定することができます。

<input name="singleErrorInput" ref={register({ required: "This is required." })} />
<input name="name" ref={register({ required: true })} />

このように、オブジェクトでrequiredやrequiredに文字列を指定するとエラーメッセージを出力することも可能です。
さらに、maxLengthやminLengthなどの標準的なバリデーションを適用したり、正規表現を使用してパターンマッチングを行うこともできます。

ただし、複数のバリデーションを追加していくとコンポーネントが大きくなり、可読性が悪くなる可能性があります。
そのため、バリデーション情報を別のファイルに切り出して使用する方法も取られています。

// validation.ts

export const firstName = {
    required: { value: true, message: '姓は必ず入力してください。' },
    minLength: { value: 3, message: 'タイトルは3文字以上入力してください。' },
    maxLength: { value: 100, message: 'タイトルは100文字以内で入力してください。' },
};

export const lastName = {
    required: { value: true, message: '名は必ず入力してください。' },
    minLength: { value: 3, message: '名は3文字以上入力してください。' },
    maxLength: { value: 100, message: '名は100文字以内で入力してください。' },
};

このように、バリデーション情報を別のファイルに切り出すことで、その情報をregisterに渡すことができます。

inputRef={register(validation.firstName)}

次に、エラーハンドリングを行ってみましょう。
useFormの初期化時にフォーム内のフィールドの型をジェネリクスとして渡しておくと、errors.まで入力すると型補完が効くようになると思います。

registerに適切なバリデーション情報を渡している場合、バリデーションが適用され、errorsオブジェクトでエラーを取得することができます。
エラーが存在する場合は、簡単にエラーメッセージを表示するなどのハンドリングが可能になります。

{errors.firstName && (
  <Box>
    <FormError errorField={errors.firstName} message={errors.firstName.message} />
  </Box>
)}

他の項目もほとんど同じ要領で可能ですが、今回のチェックボックスだけはMaterial UIのチェックボックスではなく、通常のHTMLのチェックボックス要素を使用しています。
そのため、inputRefではなくrefを使用してregisterを渡している点に注意してください。

最後のハンドリングでは、formStateから取得したisDirtyとisValidを使用しています。
isDirtyとisValidがTrueの場合、ボタンをdisabledに設定しています。
この仕様により、ユーザーがフォームのフィールドを一度は触っている状態で、かつエラーがひとつも存在しない場合にボタンがアクティブになるようになっています。

// ----------------------------------------
// submit
// ----------------------------------------
<Box mt="48px" display="flex" justifyContent="center" alignItems="center">
  <Button
    color="primary"
    type="submit"
    disabled={!(isDirty && isValid)}
  />
   送信する
  </Button>
</Box>

最後に、最も気をつけたいポイントとして、各フィールドにはdefaultValueというプロパティを設定することができます。

<InputBase
 name="firstName"
 inputRef={register(errorMessages.firstName)}
 defaultvalue=""
/>

こうすることで、フィールドにデフォルト値を設定することができます。
デフォルト値が絶対に必要な値ではない場合は、削除しても問題ありません。
しかし、defaultvalueが入っていたために謎の挙動にハマったことがあるかもしれません。そのような状況に遭遇した場合、以下の条件が該当します。

①defaultValueに空文字が入っている。
②reValidateの設定がonChangeになっている。

この2つの条件が揃った場合、少し厄介な動作が発生します。
例えば、minLengthなどの文字数制限のバリデーション条件が設定されている場合、入力時の最初の1文字目がdefaultValueの空文字に対してバリデーション判定が行われます。

バリデーションとdefaultValueの相互作用により、予期しない動作が発生し、結構ハマりやすいポイントになります。
そのため、defaultValueに注意を払ってください。
以上が、react-hook-formについての説明でした。

それでは。

この記事が気に入ったらサポートをしてみませんか?