zukucode
主にWEB関連の情報を技術メモとして発信しています。

React Hook Form + TypeScriptで共通のコントロールを作成する

React Hook Form+TypeScriptの環境で、共通のコントロールコンポーネントを作成します。

TypeScriptでフィールド名の型チェックが行われるように作成します。

ここで紹介する方法はTypeScriptのバージョンが古い場合は動作しない可能性があります。

本記事では4.8.4で動作確認を行っています。

まずはFieldByTypeという型(type)を作成します。 1

types.ts
import { 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 Formnamecontrolなどをパラメータで指定できるようにします。

それに加えて、InputHTMLAttributes<HTMLInputElement>を使用して、テキストボックスの標準のパラメータも指定できるようにします。

ただし、namevalueReact Hook Formで管理するため、型から除外しています。

InputText.tsx
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, 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;

動作確認

以下のように、フォームに存在しないフィールド名を指定するとエラーになることを確認できます。

react-hook-form型チェック1.png

また、string型でないフィールド名を指定するとエラーになることを確認できます。

FieldByType<TFieldValues, string>で、string型のみ受け付ける入力項目にしたためです。

react-hook-form型チェック2.png

ほかのコントロールの作成

チェックボックスやコンボボックスも同じ要領で作成できます。

チェックボックスの場合は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

関連記事