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 Form
のname
やcontrol
などをパラメータで指定できるようにします。
それに加えて、InputHTMLAttributes<HTMLInputElement>
を使用して、テキストボックスの標準のパラメータも指定できるようにします。
ただし、name
やvalue
はReact 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;
動作確認
以下のように、フォームに存在しないフィールド名を指定するとエラーになることを確認できます。
また、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 ↩