React Hook Formは、フォームの入力データを検証まで含めて、まとめて簡単に扱えるライブラリです。ただ、導入のページ(「はじめる」)にコード例は示されているものの、説明があまりありません。本稿は、その中から基本的なコード例8つを採り上げ、公式ドキュメントの引用やリンクも加えて解説します。コード例はわかりやすい(あるいは動く)ように手直しし、CodeSandboxにサンプルを掲げました。
インストール
React Hook Formは、npm install
コマンドでつぎのようにインストールします。
npm install react-hook-form
アプリケーションを手もとでつくるには、Create React Appを使うのがよいでしょう。本稿のコード例の場合には、TypeScriptのテンプレートを加えてください(「React + TypeScriptのひな形作成とFullCalendarのインストール」参照)。
基本的な使い方
まずは、React Hook Formの基本的な使い方です。細かな構文は、あとの項で説明します。とりあえず、フックをどのように用いるか、大まかな仕組みについてご理解ください。
コード例の動きは、つぎのCodeSandboxのサンプル001で確かめられます。また、以下のコード001がアプリケーションモジュール(src/App.tsx
)の記述です。なお、最小限のCSSの定めについては、src/styles.css
をご覧ください。
サンプル001■React + TypeScript: React Hook Form basic example
アプリケーションモジュール(src/App.tsx
)の処理の流れはつぎのとおりです。useForm
フックの戻り値から、register
を取り出します。フックで扱うフォーム要素にはregister
で一意の名前を渡して登録しなければなりません。戻り値は、登録したフォーム要素にスプレッド構文で加えてください。
useForm
フックから得たhandleSubmit
は、フォームの入力を確かめたうえで、引数に渡した関数(onSubmit)を呼び出すハンドラです。SubmitHandler
がその型づけになります。データ(data
)が出力されますので、Consoleを開いてお確かめください。
watch
は、引数に渡した名前のフォーム要素の入力値を監視して返します。formState
は、名前が示すとおりフォームの状態を情報として収めたオブジェクトです。errors
プロパティから、登録した名前ごとにエラーが取り出せます。
コード001■React Hook Formの基本的な使い方
import { useForm, SubmitHandler } from 'react-hook-form';
import './styles.css';
type Inputs = {
example: string;
exampleRequired: string;
};
export default function App() {
const {
register,
handleSubmit,
watch,
formState: { errors }
} = useForm<Inputs>();
const onSubmit: SubmitHandler<Inputs> = (data) => console.log('onSubmit:', data);
console.log('watch:', watch('example')); // watchは引数に渡した名前の入力値を監視する
return (
/* handleSubmitはフォームの入力を確かめたうえで、引数に渡した関数(onSubmit)を呼び出す */
<form onSubmit={handleSubmit(onSubmit)}>
{/* register関数の呼び出しにより、フォーム入力の要素を引数の名前で登録する */}
<input defaultValue="test" {...register('example')} />
{/* register関数の第2引数には、HTML標準フォームデータ検証のルールが渡せる */}
<input {...register('exampleRequired', { required: true })} />
{/* データ検証に失敗するとerrorsが返され、登録した名前で取り出せる */}
{errors.exampleRequired && (
<span style={{ color: 'red' }}>This field is required</span>
)}
<input type="submit" />
</form>
);
}
フォームの要素をフックに登録する
React Hook Formを用いるときに大事なのは、まず「非制御コンポーネント」をフックに登録することです。useForm
フックから取り出したregister
関数で登録し、引数にキーとなる一意の識別子(name
)を渡してください(第2引数は省略可)。関数の戻り値は、登録したフォーム要素をフックで扱うためのオブジェクトです。登録したフォーム要素にスプレッド構文で加えます。
register: (name: string, RegisterOptions?) => ({ onChange, onBlur, name, ref })
useForm
フックから得られるhandleSubmit
関数は、登録したフォーム要素のデータを検証したうえで、引数のコールバック関数により送信します(第2引数は省略可)。コールバックに渡される引数(data
)は、登録したフォーム要素の名前と値をプロパティとするオブジェクトです。
handleSubmit: ((data: Object, e?: Event) => void, (errors: Object, e?: Event) => void) => Function
CodeSandboxにサンプル002を公開しました。また、モジュールsrc/App.tsx
の中身が、以下のコード002です。
サンプル002■React + TypeScript: React Hook Form basic example 2
コード002■フックに登録したフォーム要素のデータを確かめる
import { useForm, SubmitHandler } from 'react-hook-form';
import './styles.css';
enum GenderEnum {
female = 'female',
male = 'male',
other = 'other'
}
interface IFormInput {
firstName: String;
gender: GenderEnum;
}
export default function App() {
const { register, handleSubmit } = useForm<IFormInput>();
const onSubmit: SubmitHandler<IFormInput> = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>
First Name
<input {...register('firstName')} />
</label>
<label>
Gender Selection
<select {...register('gender')}>
<option value="female">female</option>
<option value="male">male</option>
<option value="other">other</option>
</select>
</label>
<input type="submit" />
</form>
);
}
フォームのデータを検証する
register
関数の第2引数には、HTML標準フォームデータ検証のルールが渡せます(「クライアント側のフォームデータ検証」参照)。以下のサンプル003で用いたルールは、つぎの表001のとおりです(コード003)。
表001■register
関数の第2引数に渡せるHTML標準フォームデータ検証のルール(一部)
ルール | 説明 |
---|---|
required: boolean | 値が入力されなければならないかどうか |
min: number | 値の最小値 |
max: number | 値の最大値 |
minLength: number | データ長の最小値 |
maxLength: number | データ長の最大値 |
pattern: RegExp | データが合致するかどうか調べる正規表現 |
サンプル003■React + TypeScript: React Hook Form basic example 3
コード003■フォームに入力したデータをルールに照らして検証する
import { useForm, SubmitHandler } from 'react-hook-form';
import './styles.css';
interface IFormInput {
firstName: String;
lastName: string;
age: number;
}
export default function App() {
const { register, handleSubmit } = useForm<IFormInput>();
const onSubmit: SubmitHandler<IFormInput> = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>
firstName
<input {...register('firstName', { required: true, maxLength: 20 })} />
</label>
<label>
lastName
<input {...register('lastName', { pattern: /^[A-Za-z]+$/i })} />
</label>
<label>
age
<input type="number" {...register('age', { min: 18, max: 99 })} />
</label>
<input type="submit" />
</form>
);
}
コンポーネント化した要素をフォームに組み込む
React Hook Formは、フォーム要素にref
を与えて管理します。register
関数は、要素に内部的にref
を加える役割があるのです(表002参照)。子コンポーネントをフォームに組み込むには、何らかのかたちでref
を定めなければなりません。
ひとつのやり方は、親コンポーネントから子にregister
関数を渡して登録することです。以下のコード004では、モジュールsrc/Input.tsx
が親から受け取ったregister
で要素を登録しました。
表002■register
関数が返すオブジェクトのプロパティ
プロパティ | 説明 |
---|---|
onChange: ChangeHandler | 登録したフォーム要素の入力値が変わったときに呼び出すイベントハンドラ |
onBlur: ChangeHandler | 登録したフォーム要素からフォーカスが外れたときに呼び出すイベントハンドラ |
ref: React.Ref<any> | 登録したフォーム要素をフックが参照するためのrefオブジェクト |
name: string | 登録したフォーム要素に与えた名前 |
もうひとつのやり方は、親がref
を子に渡すことです。ただし、親が子に定めたref
は、子コンポーネントの引数のプロパティ(props
)からは受け取れません。
通常の関数またはクラスコンポーネントは
ref
引数を受け取らず、ref
はprops
からも利用できません。
(「DOM コンポーネントに ref をフォワーディングする」)
親が与えたref
を受け取るために用いるのがReact.forwardRef
で(「ref のフォワーディング」参照)、引数に定めるのが関数コンポーネントです。コード004のモジュールsrc/Select.tsx
は、引数の関数コンポーネントが第2引数でref
を受け取れるので、戻り値のJSXフォーム要素に定めました。なお、ref
は親からregister
の呼び出しにより与えられることは前述のとおりです(表002)。
サンプル004■React + TypeScript: React Hook Form basic example 4
コード004■要素を子コンポーネントに分けてアプリケーションのフォームに組み込む
import React from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import { Input } from './Input';
import { Select } from './Select';
import './styles.css';
export interface IFormValues {
'First Name': string;
Age: number;
}
function App() {
const { register, handleSubmit } = useForm<IFormValues>();
const onSubmit: SubmitHandler<IFormValues> = (data) => {
alert(JSON.stringify(data));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Input label="First Name" register={register} required />
<Select label="Age" {...register('Age')} />
<input type="submit" />
</form>
);
}
export default App;
import { Path, UseFormRegister } from 'react-hook-form';
import { IFormValues } from './App';
type InputProps = {
label: Path<IFormValues>;
register: UseFormRegister<IFormValues>;
required: boolean;
};
// <input>要素を含んだ子コンポーネント
export const Input = ({ label, register, required }: InputProps) => (
<label>
{label}
<input {...register(label, { required })} />
</label>
);
import React from 'react';
import { UseFormRegister } from 'react-hook-form';
import { IFormValues } from './App';
// React.forwardRefでrefオブジェクトを渡すこともできる
export const Select = React.forwardRef<
HTMLSelectElement,
{ label: string } & ReturnType<UseFormRegister<IFormValues>>
>(({ onChange, onBlur, name, label }, ref) => (
<label>
{label}
<select name={name} ref={ref} onChange={onChange} onBlur={onBlur}>
<option value="20">20</option>
<option value="30">30</option>
</select>
</label>
));
UIライブラリと組み合わせて使う
React Hook Formに、MUIやReact Selectといった他のUIライブラリのコンポーネントを組み込むこともできます。そうしたコンポーネントを包むのがController
です(表003参照)。コンポーネントに定めるrender
のコールバックでレンダープロップを受け取り、UIコンポーネントに加えて返してください。これらのプロパティにはref
も含まれます。
表003■Controller
コンポーネントに与えられるプロパティ
プロパティ | 説明 |
---|---|
name: string | フォーム入力要素に与える一意の識別子(必須) |
control: Object |
useForm の戻り値から得られるcontrol オブジェクト |
render: Function | レンダープロップ(render prop)からレンダーするReact要素を返す関数。 |
defaultValue: any | フォーム入力要素のデフォルト値 |
rules: Object | フォームデータの検証ルール(register と同じ) |
MUIのButton
とInput
およびReact Selectをフォームに組み込んだのが、つぎのサンプル005です(コード005)。render
コールバックの引数(field
)からフォーム要素に展開されるプロパティによりref
が与えられます。
サンプル005■React + TypeScript: React Hook Form basic example 5
コード005■UIライブラリの要素を組み込んで制御する
import React from 'react';
import Select from 'react-select';
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
import { Button, Input } from '@mui/material';
import './styles.css';
interface IFormInput {
firstName: string;
iceCreamType: { label: string; value: string };
}
export default function App() {
const { control, handleSubmit } = useForm<IFormInput>();
const onSubmit: SubmitHandler<IFormInput> = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>
First Name
<Controller
name="firstName"
control={control}
defaultValue=""
render={({ field }) => <Input {...field} />}
/>
</label>
<label>
Ice Cream Preference
<Controller
name="iceCreamType"
control={control}
render={({ field }) => (
<Select
{...field}
options={[
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
{ value: "vanilla", label: "Vanilla" }
]}
/>
)}
/>
</label>
<Button type="submit" variant="outlined">
送信
</Button>
</form>
);
}
制御されたコンポーネントをフォームに組み込む
React Hook Formでは、非制御コンポーネントおよびHTML標準の<input>
を用いることが想定されています。けれど、他のUIライブラリや汎用化したフォーム要素のコンポーネントを使いたいことが少なくないでしょう。その場合、組み込むコンポーネントにはref
を定めなければなりません。
Controller
コンポーネントを使う
ひとつのやり方は、前項と同じくController
で包むことです。render
コールバックが受け取った引数オブジェクトをコンポーネントに展開して与えれば、その中にref
も含まれています。
サンプル006■React + TypeScript: React Hook Form basic example 6
コード006■Controller
コンポーネントでフォーム入力要素を組み込む
import React from 'react';
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
import { TextField, Checkbox } from '@mui/material';
import './styles.css';
const defaultValues = {
TextField: '',
MyCheckbox: false
};
interface IFormInputs {
TextField: string;
MyCheckbox: boolean;
}
function App() {
const { handleSubmit, control } = useForm<IFormInputs>({
defaultValues
});
const onSubmit: SubmitHandler<IFormInputs> = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>
MUI TextField
<Controller
render={({ field }) => <TextField {...field} />}
name="TextField"
control={control}
/>
</label>
<label>
MUI Checkbox
<Controller
name="MyCheckbox"
control={control}
rules={{ required: true }}
render={({ field }) => <Checkbox {...field} />}
/>
</label>
<input type="submit" />
</form>
);
}
export default App;
このとき気をつけなければならないのは、useForm
フックにデフォルト値(defaultValues
)を渡すことです。引数なしに呼び出すと、前掲コード006ではTextField
コンポーネントについて、つぎのような警告が示されてしまいます。
Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.
なお、デフォルト値はフックでなく、Controller
コンポーネントのdefaultValue
で与えることもできます(前掲表003参照)。
useController
フックを使う
もうひとつのやり方は、子コンポーネントからuseController
フックを呼び出すことです。引数には親から受け取るプロパティを渡します。戻り値から得られるfield
にref
が含まれますので、フォーム入力の要素に展開して加えてください。なお、戻り値からfieldState
を取り出せば、入力値の検証もできます。
サンプル007■React + TypeScript: React Hook Form basic example 7
コード007■useController
フックで子コンポーネントをフォームに組み込む
import React from 'react';
import { useForm } from 'react-hook-form';
import { Input } from './Input';
import './styles.css';
export type FormValues = {
FirstName: string;
};
function App() {
const { handleSubmit, control } = useForm<FormValues>({
defaultValues: {
FirstName: ''
},
mode: 'onChange'
});
const onSubmit = (data: FormValues) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Input control={control} name="FirstName" rules={{ required: true }} />
<input type="submit" />
</form>
);
}
export default App;
import { useController, UseControllerProps } from 'react-hook-form';
import { FormValues } from './App';
export const Input = (props: UseControllerProps<FormValues>) => {
const { field, fieldState } = useController(props);
return (
<div>
<input {...field} placeholder={props.name} />
<p>{fieldState.isTouched && 'Touched'}</p>
<p>{fieldState.isDirty && 'Dirty'}</p>
<p>{fieldState.invalid ? 'invalid' : 'valid'}</p>
</div>
);
};
エラーを扱う
useForm
の戻り値から得られるformState
で、登録したフォーム要素の情報が得られます。エラーを含むのがerrors
オブジェクトです。登録した名前(name
)をプロパティとして、個別に取り出せます。
サンプル008■React + TypeScript: React Hook Form basic example 8
コード008■登録したフォーム要素のデータ検証に対するエラーを調べる
import React from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import './styles.css';
interface IFormInputs {
firstName: string;
lastName: string;
}
const onSubmit: SubmitHandler<IFormInputs> = (data) => console.log(data);
export default function App() {
const {
register,
formState: { errors },
handleSubmit
} = useForm<IFormInputs>();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('firstName', { required: true })} />
{errors.firstName && 'First name is required'}
<input {...register('lastName', { required: true })} />
{errors.lastName && 'Last name is required'}
<input type="submit" />
</form>
);
}
公式サイトについて
React Hook Formの公式サイトには日本語版があります。ただ、導入部分のページだけで、説明もあまりありまません。英語サイトの方が同じページでももう少し情報は多く、ドキュメントへのリンクも添えられています。日本語で書かれていてもそもそも情報が少ないので、ここは英語サイトを見るべきでしょう。
本稿は、Get Started
のコード例をもとに、解説を加えています。けれど、公式ページの中にはそのままコピー&ペーストして動作しないものがありました(逆に、不要なコードもあったり)。もっとも、CodeSandboxのサンプルを見ると動いています。ただ、公式ページに掲げられているコード例とかなり違っているものが少なくありません(たとえば、CSSはかなりゴテゴテに定められています)。
本稿のコード例はそのまま動くように整え、リンクしたCodeSandboxのサンプルと基本同じです(CSSは最小限加えました)。また、別モジュールにできるコンポーネントは分け、多少なりとも実践的にしました。