【React(TypeScript)】react-hook-formを使ったカスタムテキストボックスを作る

# 概要
テキストボックスを拡張したコンポーネントをreact-hook-formで使えるようにするには一工夫必要です。registerをどうやって渡したらいいのか分からないという方も多いのではないでしょうか。試行錯誤してreact-hook-formに対応したカスタムテキストボックスを作ることができましたのでマイベストプラクティスとして共有させていただきたいと思います。

# やり方
ジェネリクスを駆使してreact-hook-formに必要なパラメータを渡すようにします。今回はテキストボックスを作っていますが、ラジオボタンなど他のinputでも基本的な作りは変わらないと思います。

※以下では[use-mask-input](https://www.npmjs.com/package/use-mask-input)を用いたマスク機能を持ち、バリデーションエラーメッセージを表示できるカスタムテキストボックスを作ります。CSSフレームワークとしてtailwind CSSを使っていますので、別のCSSフレームワークを使っている場合はCSSの箇所を変更してください。

# TextBox.tsx
registerやerrorsなどreact-hook-formに必要なパラメータをPropsに定義します。型が難しいので真似て作ってください。

```jsx
import { type HTMLInputTypeAttribute } from 'react'
import {
  type UseFormRegister,
  type FieldValues,
  type FieldErrors,
  type Path,
} from 'react-hook-form'

import { useHookFormMask } from 'use-mask-input'

type InputProps = React.ComponentProps<'input'>

type Props<T extends FieldValues> = InputProps & {
  register: UseFormRegister<T>
  errors: FieldErrors<T>
  fieldName: Path<T> // instead of a string
  type?: HTMLInputTypeAttribute
  mask?: string // https://www.npmjs.com/package/use-mask-input
  maskOption?: object
}

const TextBox = <T extends FieldValues>({
  register,
  errors,
  fieldName,
  type = 'text',
  mask,
  maskOption,
  id,
  ...others
}: Props<T>) => {
  const registerWithMask = useHookFormMask(register)

  function createInput() {
    const defaultClassName =
      'mt-2 block w-full rounded-md border bg-white px-4 py-3 focus:border-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-40 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:focus:border-blue-300'

    if (mask == null) {
      return (
        <input
          type={type}
          id={id ?? fieldName}
          className={`${defaultClassName} ${getBorderColor()}`}
          {...register(fieldName)}
          {...others}
        />
      )
    } else {
      return (
        <input
          type={type}
          id={id ?? fieldName}
          className={`${defaultClassName} ${getBorderColor()}`}
          {...registerWithMask(fieldName, mask, maskOption)}
          {...others}
        />
      )
    }
  }

  function getBorderColor() {
    if (errors[fieldName] != null) {
      return 'border-error'
    }
    return 'border-gray-200'
  }

  return (
    <>
      {createInput()}
      {errors[fieldName]?.message != null && (
        <p className="text-error">{String(errors[fieldName]?.message)}</p>
      )}
    </>
  )
}

export default TextBox
```

# 使い方
registerやerrorsなどのパラメータを渡します。バリデーションのライブラリとしてはyupを使用していますが、他のライブラリでも動くと思います。

```jsx
import { useForm, type SubmitHandler } from 'react-hook-form'

import { yupResolver } from '@hookform/resolvers/yup'
import TextBox from 'components/TextBox'
import * as yup from 'yup'

type FormValues = {
  title: string
}

const informationSchema = yup.object().shape({
  title: yup.string().required().max(30),
})

export default function FormWithValidation() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({
    resolver: yupResolver(informationSchema),
  })

  const onSubmit: SubmitHandler<FormValues> = (data) => {
    console.log('submitData', data)
  }

  return (
    <div>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <TextBox register={register} errors={errors} fieldName="title" />
        </div>
        <div>
          <button type="submit">送信</button>
        </div>
      </form>
    </div>
  )
}
```