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 ↩