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"}
        ref={register({ required: true, maxLength: 30 })}
      />
      {/* use role="alert" to announce the error message */}
      {
        errors.name 
        && errors.name.type === "required"
        && (
          <span role="alert">
            This is required
          </span>
        )
      }
      {
        errors.name 
        && errors.name.type === "maxLength"
        && (
          <span role="alert">
            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: ルーティングとストアを設定します。

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";

createStore({
  data: {}
});

export default function App() {
  return (
    <StateMachineProvider>
      <Router>
        <Route exact path="/" component={Step1} />
        <Route path="/step2" component={Step2} />
        <Route path="/result" component={Result} />
      </Router>
    </StateMachineProvider>
  );
}

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

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)}>
      <input name="firstName" ref={register} />
      <input name="lastName" ref={register} />
      <input type="submit" />
    </form>
  );
};

export default withRouter(Step1);

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

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

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

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

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


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

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

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

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

  return (
    <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 のメソッドを子コンポーネントに注入することです。

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 に登録することです。

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 を使用した実装のリンクを参照して下さい。

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}>
            {/* important: useFieldArray only works with ref={register()} */}
            <input name={`test[${index}].name`} defaultValue={item.name} ref={register()} />
            <button onClick={() => remove(index)}>Delete</button>
          </li>
        ))}
      </ul>
      <button type="button" onClick={() => append({ name: "test" })} >
        append
      </button>
      <button type="button" onClick={() => prepend({ name: "test1" })}>
        prepend
      </button>
    </form>
  );
}
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 では、エラーを簡単に取得できるように 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 });
};

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, { memo } from "react";
import { useForm, FormContext, useFormContext } from "react-hook-form";

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

export const NestedInputContainer = ({ children }) => {
  const methods = useFormContext();

  return <NestedInput {...methods} />;
};

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

  return (
    <FormContext {...methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        <NestedInputContainer />
        <input type="submit" />
      </form>
    </FormProvider>
  );
}

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

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

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

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

下記に例を示します:

import * as React from "react";
import { useForm } from "react-hook-form";
import { ModalForm } from "./ModalForm";

type FormValues = {
  toggle: boolean;
  mail: string;
  ghost: string;
  keepValue: string;
};

export default function App() {
  const [modalFormData, setModalFormData] = React.useState("");
  const { watch, register, setValue, getValues, handleSubmit } = useForm<FormValues>();
  const [showModal, setShowModal] = React.useState(false);
  const { toggle, mail } = watch();
  
  React.useEffect(() => {
    setValue("mail", modalFormData);
  }, [setValue, modalFormData]);

  const onSubmit = (data: FormValues) => {
    console.log(data);
  };

  return (
    <>
      <form onSubmit={handleSubmit(onSubmit)}>
        <input type="checkbox" name="toggle" ref={register} />
        {toggle && (
          <button type="button" onClick={() => setShowModal(!showModal)}>
            Show Modal
          </button>
        )}

        <input name="mail" placeholder="mail" ref={register} />

        <input
          name="keepValue"
          placeholder="keepValue"
          ref={register}
          style={{
            display: toggle ? "block" : "none" // toggle the visbility of an input
          }}
        />

        <input type="submit" />
      </form>

      {/* working with a modal pop up, make sure to create separate form */}
      {showModal && (
        <ModalForm mail={mail} setModalFormData={setModalFormData} />
      )}
    </>
  );
}
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>
  );
}
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>
          </Select>
        }
        control={control}
        name="select"
        defaultValue={10}
      />
      
      <Input inputRef={register} name="input" />

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

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>
      </Select>
      <br />
      <Input inputRef={register} name="input" />

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


バリデーションリゾルバーを使ったカスタムフック

バリデーションリゾルバーとしてカスタムフックを構築できます。 カスタムフックは yup/Joi/Superstruct を使って、 バリデーションリゾルバーの中で使われるバリデーションメソッドに簡単に統合することができます。

  • メモ化されたバリデーションスキームを定義する (または依存関係を持たないならばコンポーネントの外にバリデーションスキームを定義する)
  • バリデーションスキームを渡してカスタムフックを使う
  • useForm フックにバリデーションリゾルバーを渡す
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 });

バーチャルリストで動かす

データの表があるシナリオを想像してください。 この表は100または1000以上の列を含み、 それぞれの列には入力欄があります。 一般的にはビューポート内にあるアイテムのみをレンダリングしますが、 これはアイテムがビューの外に出た時にDOMから削除されて、 再追加されるため問題が発生します。 これはアイテムが再びビューポートに入った時に、 アイテムがデフォルトの値にリセットされる原因となります。

この問題を回避するためには、 手動でフィールドを登録し、 プログラムによってフィールドの値をセットします。

以下に react-window を使用した例を示します。

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>
      <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(() => {
    items.forEach((item, idx) => {
      formMethods.register("[${idx}].quantity");
    });
  }, [formMethods, items]);

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

フォームをテストする

テストはバグやミスを防いだり、 コードをリファクタリングする時にコードの安全性を保証するため、 とても重要なものです。

私たちは testing-library を使うことをお勧めします。なぜなら、テストコードはシンプルで、そしてテストはよりユーザーの行動にフォーカスしています。

Step 1: テスト環境を設定する

react-hook-form は DOM からアンマウントされた input 要素を検出するために MutationObserver を使うため @testing-library/jest-dom jest の最新バージョンとともにインストールしてください。

npm install -D @testing-library/jest-dom

そして @testing-library/jest-dom をインポートするために setup.js を作成してください。

import "@testing-library/jest-dom";

最後に、setup.jsjest.config.js で読み込む必要があります。

module.exports = {
  setupFilesAfterEnv: ["<rootDir>/setup.ts"]
  // ...other settings
};

Step 2:ログインフォームを作成する

私たちは、role 属性を設定しています。それらの属性はテストを書いたり、アクセシビリティーを改善する時に役に立ちます。 詳しくは testing-library のドキュメントを参照してください。

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

export default function App({ login }) {
  const { register, handleSubmit, errors, reset } = useForm();
  const onSubmit = async data => {
    await login(data.email, data.password);
    reset();
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label htmlFor="email">email</label>
      <input
        id="email"
        name="email"
        ref={register({
          required: "required",
          pattern: {
            value: /S+@S+.S+/,
            message: "Entered value does not match email format"
          }
        })}
        type="email"
      />
      {errors.email && <span role="alert">{errors.email.message}</span>}
      <label htmlFor="password">password</label>
      <input
        id="password"
        name="password"
        ref={register({
          required: "required",
          minLength: {
            value: 5,
            message: "min length is 5"
          }
        })}
        type="password"
      />
      {errors.password && <span role="alert">{errors.password.message}</span>}
      <button type="submit">SUBMIT</button>
    </form>
  );
}

Step 3: テストを書く

テストでカバーしようとしているのは以下の条件です:

  • 送信時のテストに失敗

    handleSubmit は非同期で実行されるので、 送信したことを検出するために waitFor find* メソッドを使います。

  • それぞれの入力に関するバリデーションをテストする

    異なる要素を探す時に *ByRole を使います。なぜなら、このようにしてユーザーはUIコンポーネントを認識するからです。

  • 送信時のテストに成功

import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import App from "./App";

const mockLogin = jest.fn((email, password) => {
  return Promise.resolve({ email, password });
});

describe("App", () => {
  beforeEach(() => {
    render(<App login={mockLogin} />);
  });
  
  it("should display required error when value is invalid", async () => {
    fireEvent.submit(screen.getByRole("button"));

    expect(await screen.findAllByRole("alert")).toHaveLength(2);
    expect(mockLogin).not.toBeCalled();
  });

  it("should display matching error when email is invalid", async () => {
    fireEvent.input(screen.getByRole("textbox", { name: /email/i }), {
      target: {
        value: "test"
      }
    });

    fireEvent.input(screen.getByLabelText("password"), {
      target: {
        value: "password"
      }
    });

    fireEvent.submit(screen.getByRole("button"));

    expect(await screen.findAllByRole("alert")).toHaveLength(1);
    expect(mockLogin).not.toBeCalled();
    expect(screen.getByRole("textbox", { name: /email/i }).value).toBe("test");
    expect(screen.getByLabelText("password").value).toBe("password");
  });

  it("should display min length error when password is invalid", async () => {
    fireEvent.input(screen.getByRole("textbox", { name: /email/i }), {
      target: {
        value: "test@mail.com"
      }
    });

    fireEvent.input(screen.getByLabelText("password"), {
      target: {
        value: "pass"
      }
    });

    fireEvent.submit(screen.getByRole("button"));

    expect(await screen.findAllByRole("alert")).toHaveLength(1);
    expect(mockLogin).not.toBeCalled();
    expect(screen.getByRole("textbox", { name: /email/i }).value).toBe(
      "test@mail.com"
    );
    expect(screen.getByLabelText("password").value).toBe("pass");
  });

  it("should not display error when value is valid", async () => {
    fireEvent.input(screen.getByRole("textbox", { name: /email/i }), {
      target: {
        value: "test@mail.com"
      }
    });

    fireEvent.input(screen.getByLabelText("password"), {
      target: {
        value: "password"
      }
    });

    fireEvent.submit(screen.getByRole("button"));

    await waitFor(() => expect(screen.queryAllByRole("alert")).toHaveLength(0));
    expect(mockLogin).toBeCalledWith("test@mail.com", "password");
    expect(screen.getByRole("textbox", { name: /email/i }).value).toBe("");
    expect(screen.getByLabelText("password").value).toBe("");
  });
});

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

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