LoginSignup
198
166

More than 1 year has passed since last update.

React + TypeScript: React Hook Formでフォーム入力値をまとめて簡単に取得・検証する

Posted at

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

2112001_007.png
>> CodeSandboxへ

アプリケーションモジュール(src/App.tsx)の処理の流れはつぎのとおりです。useFormフックの戻り値から、registerを取り出します。フックで扱うフォーム要素にはregisterで一意の名前を渡して登録しなければなりません。戻り値は、登録したフォーム要素にスプレッド構文で加えてください。

useFormフックから得たhandleSubmitは、フォームの入力を確かめたうえで、引数に渡した関数(onSubmit)を呼び出すハンドラです。SubmitHandlerがその型づけになります。データ(data)が出力されますので、Consoleを開いてお確かめください。

watchは、引数に渡した名前のフォーム要素の入力値を監視して返します。formStateは、名前が示すとおりフォームの状態を情報として収めたオブジェクトです。errorsプロパティから、登録した名前ごとにエラーが取り出せます。

コード001■React Hook Formの基本的な使い方

src/App.tsx
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

2112001_001.png
>> CodeSandboxへ

コード002■フックに登録したフォーム要素のデータを確かめる

src/App.tsx
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

2112001_002.png
>> CodeSandboxへ

コード003■フォームに入力したデータをルールに照らして検証する

src/App.tsx
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引数を受け取らず、refpropsからも利用できません。
(「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

2112001_004.png
>> CodeSandboxへ

コード004■要素を子コンポーネントに分けてアプリケーションのフォームに組み込む

src/App.tsx
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;
src/Input.tsx
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>
);
src/Select.tsx
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に、MUIReact 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のButtonInputおよびReact Selectをフォームに組み込んだのが、つぎのサンプル005です(コード005)。renderコールバックの引数(field)からフォーム要素に展開されるプロパティによりrefが与えられます。

サンプル005■React + TypeScript: React Hook Form basic example 5

2112001_005.png
>> CodeSandboxへ

コード005■UIライブラリの要素を組み込んで制御する

src/App.tsx
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

2112001_008.png
>> CodeSandboxへ

コード006■Controllerコンポーネントでフォーム入力要素を組み込む

src/App.tsx
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フックを呼び出すことです。引数には親から受け取るプロパティを渡します。戻り値から得られるfieldrefが含まれますので、フォーム入力の要素に展開して加えてください。なお、戻り値からfieldStateを取り出せば、入力値の検証もできます。

サンプル007■React + TypeScript: React Hook Form basic example 7

2112001_006.png
>> CodeSandboxへ

コード007■useControllerフックで子コンポーネントをフォームに組み込む

src/App.tsx
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;
src/Input.tsx
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

2112001_009.png
>> CodeSandboxへ

コード008■登録したフォーム要素のデータ検証に対するエラーを調べる

src/App.tsx
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は最小限加えました)。また、別モジュールにできるコンポーネントは分け、多少なりとも実践的にしました。

198
166
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
198
166