Skip to content

高度な使用法

React Hook Formを使用して、複雑でアクセス可能なフォームを構築する。

アクセシビリティ (A11y)

React Hook Form はネイティブフォームバリデーションをサポートします。 これにより、独自のルールで input のバリデーションを行うことができます。 私たちのほとんどはカスタムデザインとレイアウトを適用してフォームを構築しますが、 フォームのアクセシビリティ (A11y) を保証することも私たちの責任です。

下記のコードの例は、意図したとおりのバリデーションが動作しますが、 アクセシビリティについては改良することができます。

import React from "react";
import { useForm } from "react-hook-form";

export default function App() {
  const { register, handleSubmit, errors } = useForm();
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label for="name">Name</label>
      <input type="text" id="name" ref={register({ required: true, maxLength: 30 })} />
      {errors.name && errors.name.type === 'required' && <span>This is required</span>}
      {errors.name && errors.name.type === 'maxLength' && <span>Max length exceeded</span> }
      <input type="submit" />
    </form>
  );
}

下記のコードの例は、 ARIA を活用した改良版です。

import React from "react";
import { useForm } from "react-hook-form";

export default function App() {
  const { register, handleSubmit, errors } = useForm();
  const onSubmit = data => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label for="name">Name</label>
      <input
        type="text"
        id="name"
        {/* use aria-invalid to indicate field contain error */}
        aria-invalid={errors.name ? 'true' : 'false'}
        {/* use aria-describedby to associate with error messages */}
        aria-describedby="error-name-required error-name-maxLength"
        ref={register({ required: true, maxLength: 30 })}
      />
      {/* the id field is used to associated with aria-describedby*/}
      <span
        role="alert"
        id="error-name-required"
        style={{
          display: errors.name && errors.name.type === "required"
            ? "block"
            : "none"
        }}
      >
        This is required
      </span>
      <span
        role="alert"
        id="error-name-maxLength"
        style={{
          display: errors.name && errors.name.type === "maxLength"
            ? "block"
            : "none"
        }}
      >
        Max length exceeded
      </span>
      <input type="submit" />
    </form>
  );
}

この改良後、スクリーンリーダーはこのように話すでしょう: “Name, edit, invalid entry, This is required.”


ウィザードフォーム・ファンネル

In this video tutorial, I have demonstrated the core concept of how to build multiple steps funnel with React Hook Form.

異なるページやセクション間でユーザーの情報を収集することは非常に一般的です。 このような場合、異なるページやセクション間でのユーザーの入力値を、 状態管理ライブラリを使用して保存しておくことをお勧めします。 この例では、状態管理ライブラリとして little state machine (より身近なものであれば、 redux として置き換えることができます) を使用します。

ステップ1: ルーティングとストアを設定します。

CodeSandbox
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import { StateMachineProvider, createStore } from "little-state-machine";
import Step1 from "./Step1";
import Step2 from "./Step2";
import Result from "./Result";

import "./styles.css";

createStore({
  data: {}
});

export default function App() {
  return (
    <StateMachineProvider>
      <h1>Page Form Wizard</h1>

      <Router>
        <Route exact path="/" component={Step1} />
        <Route path="/step2" component={Step2} />
        <Route path="/result" component={Result} />
      </Router>
    </StateMachineProvider>
  );
}

ステップ2: ページを作成し、フォームの送信データを収集し、 そのデータをストアに送信して次のページに移動するようにします。

CodeSandbox
import React from "react";
import { useForm } from "react-hook-form";
import { withRouter } from "react-router-dom";
import { useStateMachine } from "little-state-machine";
import updateAction from "./updateAction";

const Step1 = props => {
  const { register, handleSubmit } = useForm();
  const { action } = useStateMachine(updateAction);
  const onSubmit = data => {
    action(data);
    props.history.push("./step2");
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h2>Step 1</h2>
      <label>
        First Name:
        <input name="firstName" ref={register} />
      </label>
      <label>
        Last Name:
        <input name="lastName" ref={register} />
      </label>
      <input type="submit" />
    </form>
  );
};

export default withRouter(Step1);

ステップ3: 最終的に、ストア内のすべてのフォームデータを使用して、 フォームを送信したりフォームデータの結果を表示します。

CodeSandbox
import React from "react";
import { useStateMachine } from "little-state-machine";
import updateAction from "./updateAction";

const Step1 = props => {
  const { state } = useStateMachine(updateAction);

  return (
    <>
      <h2>Result:</h2>
      <pre>{JSON.stringify(state, null, 2)}</pre>
    </>
  );
};

上記のパターンに従って、複数のページ間でのユーザーの入力データを収集して、 ウィザードフォーム・ファンネルを構築できるはずです。


スマートフォームコンポーネント

ここでのアイデアは、input とフォームを簡単に組み合わせることができるということです。Form コンポーネントを作成して、フォームデータを自動的に収集します。

CodeSandbox
import React from 'react'
import { Form, Input, Select } from './Components';

export default function App() {
  const onSubmit = data => console.log(data);

  return (
    <>
      <h1>Smart Form Component</h1>
      <Form onSubmit={onSubmit}>
        <Input name="firstName" />
        <Input name="lastName" />
        <Select name="sex" options={["female", "male"]} />

        <Input type="submit" value="Submit" />
      </Form>
    </>
  );
}

各コンポーネントがどのように構成されているか見てみましょう。

Form

Form コンポーネントの責任は、全ての react-hook-form のメソッドを子コンポーネントに注入することです。

CodeSandbox
import React from "react";
import { useForm } from "react-hook-form";

export default function Form({ defaultValues, children, onSubmit }) {
  const methods = useForm({ defaultValues });
  const { handleSubmit } = methods;

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {Array.isArray(children)
        ? children.map(child => {
            return child.props.name
              ? React.createElement(child.type, {
                  ...{
                    ...child.props,
                    register: methods.register,
                    key: child.props.name
                  }
                })
              : child;
          })
        : children}
    </form>
  );
}

Input / Select

Input / Select コンポーネントの責任は、自分自身を react-hook-form に登録することです。

CodeSandbox
import React from "react";

export function Input({ register, name, ...rest }) {
  return <input name={name} ref={register} {...rest} />;
}

export function Select({ register, options, name, ...rest }) {
  return (
    <select name={name} ref={register} {...rest}>
      {options.map(value => (
        <option value={value}>{value}</option>
      ))}
    </select>
  );
}

Form コンポーネントを使用して、react-hook-formprops を子コンポーネントに注入することで、 アプリケーションで複雑なフォームを簡単に作成及び組み合わせることができます。


フィールド配列

この機能は、React Hook Formが提供する最良の機能の一つです。 この機能を実現するために (他のライブラリのように) コンポーネントをインポートする代わりに、 既存の HTML マークアップを活用することができます。 key は、 name 属性にあります。 React Hook Form において、 name 属性はあなたが使用したいデータ構造を表します。

注意: 私たちは、複雑なシナリオのためのカスタムフック useFieldArray も作成しました。

下記の例は、input の name 属性を操作してどのようにフィールド配列を作成できるかを示しています。

注意: アプリケーションにフィールドの削除や挿入、追加、先頭に追加などの機能が必要な場合は、 Controller を使用した実装のリンクを参照して下さい。

CodeSandbox
import React from "react";
import { useForm, useFieldArray } from "react-hook-form";

function App() {
  const { register, control, handleSubmit } = useForm({
    // defaultValues: {}; you can populate the fields by this attribute 
  });
  const { fields, append, prepend, remove } = useFieldArray({
    control,
    name: "test"
  });
  return (
    <form onSubmit={handleSubmit(data => console.log("data", data))}>
      <ul>
        {fields.map((item, index) => (
          <li key={item.id}>
            <input name={`test[${index}].name`} ref={register} /> 
            <button onClick={() => remove(index)}>Delete</button>
          </li>
        ))}
      </ul>
      <section>
        <button type="button" onClick={() => append({ name: "test" })} >
          append
        </button>
        <button type="button" onClick={() => prepend({ name: "test1" })}>
          prepend
        </button>
      </section>
    </form>
  );
}
CodeSandbox
import React, { useState } from "react";
import { useForm } from "react-hook-form";

function createArrayWithNumbers(length) {
  return Array.from({ length }, (_, k) => k + 1);
}

export default function App() {
  const { register, handleSubmit } = useForm();
  const [size, setSize] = useState(1);
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {createArrayWithNumbers(size).map(index => {
        return (
          <>
            <label htmlFor="firstName">First Name</label>
            <input
              name={`firstName[${index}]`}
              placeholder="first name"
              ref={register({ required: true })}
            />
            
            <label htmlFor="lastName">Last Name</label>
            <input
              name={`lastName[${index}]`}
              placeholder="last name"
              ref={register({ required: true })}
            />
          </>
        );
      })}

      <button type="button" onClick={() => setSize(size + 1)} >
        Add Person
      </button>
      
      <input type="submit" />
    </form>
  );
}

スキーマバリデーション

React Hook Form は、 Yup を活用してスキーマベースのフォームバリデーションをサポートしています。 省略可能な引数として validationSchema useForm に渡すことができます。 React Hook Form は、入力されたデータをスキーマに対してバリデーションを行い、 errors や妥当な結果を返します。

ステップ1: Yup をプロジェクトにインストールします。

npm install yup

ステップ2: バリデーション用のスキーマを作成し、 React Hook Form を使用して input を登録します。

CodeSandbox
import React from "react";
import { useForm } from "react-hook-form";
import * as yup from "yup";

const schema = yup.object().shape({
  firstName: yup.string().required(),
  age: yup
    .number()
    .required()
    .positive()
    .integer(),
});

export default function App() {
  const { register, handleSubmit, errors } = useForm({
    validationSchema: schema
  });
  const onSubmit = data => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label>
        First Name
        <input type="text" name="firstName" ref={register} />
      </label>
      {errors.firstName && <p>{errors.firstName.message}</p>}
        
      <label>
        Age
        <input type="text" name="age" ref={register} />
      </label>
      {errors.age && <p>{errors.age.message}</p>}
      
      <input type="submit" />
    </form>
  );
}

エラーメッセージ

エラーメッセージは、入力に関連する問題があるときにユーザーに視覚的なフィードバックを与えることです。 React Hook Form では、エラーを簡単に取得できるように errors オブジェクトを提供しています。 ただし、画面のレンダリングエラーを改善する方法はいくつかあります。

  • Register

    register 時にエラーメッセージを埋め込み、 value 属性にエラーメッセージを簡単に挿入することができます。例:

    <input name="test" ref={register({ maxLength: { value: 2, message: "error message" } })} />

  • Optional Chaining

    Optional chaining 演算子である ?. は、 null または undefined によって発生するエラーを気にせずに errors オブジェクトを読み取ることができます。

    errors?.firstName?.message

  • Lodash get

    プロジェクトで lodash を使用している場合、lodash の get 関数を活用することができます。例:

    get(errors, 'firstName.message')


接続フォーム

フォームを作成するときに、深くネストされたコンポーネントツリーの中に input が存在することがあり、 そのような場合は FormContext が非常に便利です。ConnectForm コンポーネントを作成して React のrenderProps を活用することで、 DX を更に向上することができます。 ConnectForm コンポーネントの利点は、input をどこからでも React Hook Form に接続できることです。

import { FormContext, useForm, useFormContext } from 'react-hook-form';

export const ConnectForm = ({ children }) => {
 const methods = useFormContext();
 
 return children({
   ...methods
 });
};

// The following example will be the usage
export const DeepNest = () => {
  <ConnectForm>
    {({ register }) => <input ref={register} name="deepNestedInput" />}
  </ConnectForm>
}

export const App = () => {
  const methods = useForm();
  
  return (
    <FormContext {...methods} >
      <form>
       <DeepNest />
      </form>
    </FormContext>
  )
}

FormContext パフォーマンス

React Hook Form の FormContext は、 React の Context API 上に構築されています。 これにより、全ての階層で手動で props を渡す必要なく、 コンポーネントツリーを介してデータを渡す問題を解決します。 これにより、React Hook Form は状態を更新する度に、 コンポーネントツリーが再レンダリングされる問題を引き起こしますが、 必要に応じて下記の例のようにアプリを最適化することができます。


import React from "react";
import { useForm, FormContext, useFormContext } from "react-hook-form";

export default function App() {
  const methods = useForm();
  const onSubmit = data => console.log(data);

  return (
    <FormContext {...methods}>
      // pass all methods into the context
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        <NestedInput />
        <input type="submit" />
      </form>
    </FormContext>
  );
}

function NestedInput() {
  const {
    register,
    formState: { dirty }
  } = useFormContext();
  
  // we can use React.memo to prevent re-render except dirty state changed
  return React.useMemo(
    () => (
      <div>
        <input name="test" ref={register} />
        {dirty && <p>This field is dirty</p>}
      </div>
    ),
    [dirty]
  );
}


条件付き制御されたコンポーネント

React Hook Form を使用すると、条件付きフィールドを非常にシンプルに扱えます。 input がコンポーネントツリーから削除されると、自動的に unregister されるからです。 そのような動作をこちらの例に示します 。ただし、制御されたコンポーネントでは ref が登録されていないため、 同じように自動的に登録解除されません。 対処方法は下記の通りです。

  • Controller をインポートしてコンポーネントをラップし、 登録および登録解除を管理できるようにします

  • useEffect を使用して、カスタム登録として input を登録し、 コンポーネントのアンマウント後に登録を解除します

下記に例を示します:

import React, { useEffect } from "react";
import { useForm, Controller } from "react-hook-form";
import { TextField } from "@material-ui/core";

function App() {
  const { register, handleSubmit, setValue, watch, control } = useForm();
  const name = watch("name");
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>

      <label>Name:</label>
      <input ref={register} name="name" />

      <label>Conditional Field:</label>
      {name !== "bill" && (
        <Controller
          name="test" 
          as={TextField}
          control={control} 
          defaultValue=""
        />
      )}

      <input type="submit" />
    </form>
  );
}
CodeSandbox
import React, { useEffect } from "react";
import { useForm } from "react-hook-form";

const CustomInput = React.memo(({ register, unregister, setValue, name }) => {
  useEffect(() => {
    register({ name });
    return () => unregister(name);
  }, [name, register, unregister]);
  
  return <input onChange={e => setValue(name, e.target.value)} />;
});

function App() {
  const { register, unregister, handleSubmit, setValue, watch } = useForm();
  const name = watch("name");
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h1>Unregister Controlled Component</h1>

      <label>Name:</label>
      <input ref={register} name="name" />

      <label>Conditional Field:</label>
      {name !== "bill" && (
        <CustomInput {...{ register, unregister, setValue, name: "test" }} />
      )}

      <input type="submit" />
    </form>
  );
}

制御されたコンポーネントと非制御コンポーネントの組み合わせ

React Hook Form は、非制御コンポーネントをサポートしていますが、 制御されたコンポーネントとも互換性があります。 Material-UI Antd などの UI ライブラリのほとんどは、 制御されたコンポーネントのみをサポートして構築されています。 さらに、React Hook Form を使用することで制御されたコンポーネントの再レンダリングも最適化されます。 下記は、制御されたコンポーネントと非制御コンポーネントのフォームバリデーションを組み合わせた例です。

import React, { useEffect } from "react";
import { Input, Select, MenuItem } from "@material-ui/core";
import { useForm, Controller } from "react-hook-form";

const defaultValues = {
  select: "",
  input: ""
};

function App() {
  const { handleSubmit, reset, watch, control } = useForm({ defaultValues });
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        as={
          <Select>
            <MenuItem value={10}>Ten</MenuItem>
            <MenuItem value={20}>Twenty</MenuItem>
            <MenuItem value={30}>Thirty</MenuItem>
          </Select>
        }
        control={control}
        name="select"
        defaultValue={10}
      />
      
      <Input inputRef={register} name="input" />

      <button type="button" onClick={() => reset({ defaultValues })}>Reset</button>
      <input type="submit" />
    </form>
  );
}

CodeSandbox
import React, { useEffect } from "react";
import { Input, Select, MenuItem } from "@material-ui/core";
import { useForm } from "react-hook-form";

const defaultValues = {
  select: "",
  input: ""
};

function App() {
  const { register, handleSubmit, setValue, reset, watch } = useForm({ defaultValues });
  const selectValue = watch("select");
  const onSubmit = data => console.log(data);

  useEffect(() => {
    register({ name: "select" });
  }, [register]);

  const handleChange = e => setValue("select", e.target.value);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Select value={selectValue} onChange={handleChange}>
        <MenuItem value={10}>Ten</MenuItem>
        <MenuItem value={20}>Twenty</MenuItem>
        <MenuItem value={30}>Thirty</MenuItem>
      </Select>
      <br />
      <Input inputRef={register} name="input" />

      <button type="button" onClick={() => reset({ ...defaultValues })}>Reset</button>
      <input type="submit" />
    </form>
  );
}


Custom Hook with Validation Resolver

You can build a custom hook as a validation resolver. A custom hook can easily integration with yup/Joi/Superstruct as a validation method, and to be used inside validation resolver.

  • Define a memoized validation schema (or define it outside your component if you don't have any dependencies)
  • Use the custom hook, by passing the validation schema
  • Pass the validation resolver to the useForm hook
import { useCallback } from "react";
import { useForm } from "react-hook-form";

const useYupValidationResolver = validationSchema =>
  useCallback(
    async data => {
      try {
        const values = await validationSchema.validate(data, {
          abortEarly: false
        });

        return {
          values,
          errors: {}
        };
      } catch (errors) {
        return {
          values: {},
          errors: errors.inner.reduce(
            (allErrors, currentError) => ({
              ...allErrors,
              [currentError.path]: {
                type: currentError.type ?? "validation",
                message: currentError.message
              }
            }),
            {}
          )
        };
      }
    },
    [validationSchema]
  );

const validationSchema = useMemo(
  () =>
    yup.object({
      firstName: yup.string().required("Required"),
      lastName: yup.string().required("Required")
    }),
  []
);

const validationResolver = useYupValidationResolver(validationSchema);

const form = useForm({ validationResolver });

Working with virtualized list

Imagine a scenario where you have a table of data. This table might contain hundreds or thousands of rows, and each row will have inputs. A common practice is to only render the items that are in the viewport, however this will cause issues as the items are removed from the DOM when they are out of view, and re-added. This will cause items to reset to their default values when they re-enter the viewport.

To work around this, you can manually register the fields, and then programatically set the value of the field.

An example is shown below using react-window.

CodeSandbox
import React from "react";
import { FormContext, useForm, useFormContext } from "react-hook-form";
import { VariableSizeList as List } from "react-window";

const items = Array.from(Array(1000).keys()).map((i) => ({
  title: "List ${i}",
  quantity: Math.floor(Math.random() * 10),
}));

const WindowedRow = React.memo(({ index, style, data, getValues, setValue }) => {
  const values = getValues();
  const qtyKey = "[${index}].quantity";
  const qty = values[qtyKey];

  return (
    <div>
      <span>{data[index].title}</span>
      <input
        // Rather than ref={register}, we use defaultValue and setValue
        defaultValue={qty}
        onChange={(e) => {
          setValue(
            qtyKey,
            isNaN(Number(e.target.value)) ? 0 : Number(e.target.value)
          );
        }}
      />
    </div>
  );
});

export default React.memo(({ items }) => {
  const formMethods = useForm({ defaultValues: items });
  const onSubmit = (data) => console.log(data);

  // We manually call register here for each field.
  React.useEffect(() => {
    if (items) {
      items.forEach((item, idx) => {
        formMethods.register("[${idx}].quantity");
      });
    }
  }, [formMethods, items]);

  return (
    <form onSubmit={formMethods.handleSubmit(onSubmit)}>
      <List
        height={300}
        itemCount={items.length}
        itemSize={() => 50}
        width={300}
        itemData={items}
        {{ ...formMethods }}
      >
        {WindowedRow}
      </List>
      <button type="submit">Submit</button>
    </form>
  );
});

あなたのサポートが必要です

React プロジェクトで React Hook Form が役立つと思う場合は、リポジトリとコントリビューターをサポートするためにスターを付けてください ❤