React Hook Form + TypeScriptで共通のコントロールを作成する
React Hook Form+TypeScriptの環境で、共通のコントロールコンポーネントを作成します。
TypeScriptでフィールド名の型チェックが行われるように作成します。
ここで紹介する方法はTypeScriptのバージョンが古い場合は動作しない可能性があります。
本記事では4.8.4で動作確認を行っています。
まずはFieldByTypeという型(type)を作成します。 1
types.tsimport { FieldPathValue, FieldValues, Path } from 'react-hook-form';
export type FieldByType<FormData extends FieldValues, T> = {
  [P in Path<FormData>]: T extends FieldPathValue<FormData, P> ? P : never;
}[Path<FormData>];テキストボックスコンポーネントの作成
次にテキストボックスの共通コンポーネントを作成します。
FieldByType<TFieldValues, string>で、string型のみ受け付ける入力項目となります。
UseControllerPropsを使用して、React Hook Formのnameやcontrolなどをパラメータで指定できるようにします。
それに加えて、InputHTMLAttributes<HTMLInputElement>を使用して、テキストボックスの標準のパラメータも指定できるようにします。
ただし、nameやvalueはReact Hook Formで管理するため、型から除外しています。
InputText.tsximport { InputHTMLAttributes, useCallback } from 'react';
import { FieldValues, useController, UseControllerProps } from 'react-hook-form';
import { FieldByType } from './type'; // 上記で作成したタイプ
type Props<TFieldValues extends FieldValues, TName extends FieldByType<TFieldValues, string>> = UseControllerProps<
  TFieldValues,
  TName
> &
  Exclude<Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'name'>, UseControllerProps<TFieldValues, TName>>;
const InputText = <TFieldValues extends FieldValues, TName extends FieldByType<TFieldValues, string>>(
  props: Props<TFieldValues, TName>
) => {
  const { name, control, rules, onChange, onBlur, ...fieldProps } = props;
  const { field } = useController<TFieldValues, TName>({ control, name, rules });
  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      field.onChange(e.target.value);
      if (onChange) onChange(e);
    },
    [field, onChange]
  );
  const handleBlur = useCallback(
    (e: React.FocusEvent<HTMLInputElement, Element>) => {
      field.onBlur();
      if (onBlur) onBlur(e);
    },
    [field, onBlur]
  );
  return (
    <input
      name={field.name}
      ref={field.ref}
      value={field.value}
      onChange={handleChange}
      onBlur={handleBlur}
      {...fieldProps}
    ></input>
  );
};
export default InputText;使い方
以下のように使用します。
nameにはフィールド名、controlにはuseFormから取得したcontrol変数を指定します。
import { useForm } from 'react-hook-form';
import InputText from './InputText'; // 上記で作成したコンポーネント
type FormInput = {
  name: string;
  age: number;
};
const Test = () => {
  const { control } = useForm<FormInput>({
    defaultValues: {
      name: '',
      age: 0,
    },
  });
  return (
    <>
      <InputText name="name" control={control}></InputText>
    </>
  );
};
export default Test;動作確認
以下のように、フォームに存在しないフィールド名を指定するとエラーになることを確認できます。

また、string型でないフィールド名を指定するとエラーになることを確認できます。
FieldByType<TFieldValues, string>で、string型のみ受け付ける入力項目にしたためです。

ほかのコントロールの作成
チェックボックスやコンボボックスも同じ要領で作成できます。
チェックボックスの場合はstring型ではなくboolean型になるため以下のようになります。
import { InputHTMLAttributes, useCallback } from 'react';
import { FieldValues, useController, UseControllerProps } from 'react-hook-form';
import { FieldByType } from './type';
type Props<TFieldValues extends FieldValues, TName extends FieldByType<TFieldValues, boolean>> = UseControllerProps<
  TFieldValues,
  TName
> &
  Exclude<Omit<InputHTMLAttributes<HTMLInputElement>, 'checked' | 'name'>, UseControllerProps<TFieldValues, TName>>;
const Checkbox = <TFieldValues extends FieldValues, TName extends FieldByType<TFieldValues, boolean>>(
  props: Props<TFieldValues, TName>
) => {
  const { name, control, rules, onChange, onBlur, ...fieldProps } = props;
  const { field } = useController<TFieldValues, TName>({ control, name, rules });
  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      field.onChange(e.target.checked);
      if (onChange) onChange(e);
    },
    [field, onChange]
  );
  const handleBlur = useCallback(
    (e: React.FocusEvent<HTMLInputElement, Element>) => {
      field.onBlur();
      if (onBlur) onBlur(e);
    },
    [field, onBlur]
  );
  return (
    <input
      type="checkbox"
      name={field.name}
      ref={field.ref}
      checked={field.value}
      onChange={handleChange}
      onBlur={handleBlur}
      {...fieldProps}
    ></input>
  );
};
export default Checkbox;今回はプレーンなinput要素で作成しましたが、外部のコンポーネントライブラリにも応用できると思います。
- 参考記事: https://zenn.dev/dqn/articles/59b4f12ad37b6b ↩