NextでのFormのライブラリ選定をするためにFormikとReact Hook Formを実装してみて

2021/01/08に公開

概要

個人的には下記の観点から React Hook Form を推す方向で進めようと思った

  • React Hook Formはドキュメントがおしゃんで、さらに日本語対応してる
  • useFormContext や FormProvider など、既存の React Context と同じような使用感でコードをわかりやすくかけた(とっつきやすかった)
  • ref={register}がめちゃめちゃ楽

比較

input 入力時のレンダリング回数等のパフォーマンスの計測はしてませぬ
あくまで実装の違いのみ
ただ React Hook Form のドキュメントの中でわかりやすい比較表を作ってくれてるのでそれが参考になるかと(いや、ほんとは自分で調べないといけないと思ってるんですよ・・わかってるんですよ・・)

環境

package.json
{
  "dependencies": {
    "@hookform/resolvers": "1.3.2",
    "formik": "2.2.6",
    "next": "10.0.3",
    "react": "17.0.1",
    "react-dom": "17.0.1",
    "react-hook-form": "6.14.0",
    "yup": "0.32.8"
  }
}

全コード

Formik

インストール

  1. yarn add formik yup
  2. yarn add -D @types/yup

基本

useFormikを使った hook での書き方と<Formik /><Field />を使ったコンポーネントでの書き方二つある

hook を使った書き方はチュートリアルのコードを参考に。
今回は慣れ親しんだコンポーネントでの書き方を記載する

一つの form を presenter と container と<Formik />コンポーネントを定義する部分の三つに分ける
それぞれざっくり下記の役割

  • presenter→form の ui
  • container→useEffect とか副作用
  • <Formik />

presenter

チュートリアルから抜粋
Form や Field は formik から import したもの

export type ExampleFormValues = {
  firstName: string;
  lastName: string;
  email: string;
};

export const ExampleFormPresenter = (): JSX.Element => (
  <Form>
    <label htmlFor="firstName">First Name</label>
    <Field name="firstName" type="text" />
    <ErrorMessage name="firstName" />
    <label htmlFor="lastName">Last Name</label>
    <Field name="lastName" type="text" />
    <ErrorMessage name="lastName" />
    <label htmlFor="email">Email Address</label>
    <Field name="email" type="email" />
    <ErrorMessage name="email" />
    <button type="submit">Submit</button>
  </Form>
);

container

例)親コンポーネント側で props を通じて form の値を変えたい時

export type ExampleFormContainerProps = {
  formValues?: ExampleFormValues;
};

export const ExampleFormContainer = ({
  formValues,
}: ExampleFormContainerProps): JSX.Element => {
  const { setValues } = useFormikContext<ExampleFormValues>();

  useEffect(() => {
    setValues(formValues);
  }, [formValues]);

  return <ExampleFormPresenter />;
};

<Formik />

container を<Formik />でラップする

export type ExampleFormProps = {
  handleSubmit: (values: ExampleFormValues) => void;
  formValues?: ExampleFormValues;
};

export const ExampleForm = ({
  handleSubmit,
  formValues,
}: ExampleFormProps): JSX.Element => {
  const onSubmit = (values: ExampleFormValues) => {
    handleSubmit(values);
  };

  const initialValues: ExampleFormValues = formValues || {
    firstName: '';
    lastName: '';
    email: '';
  };

  return (
    <Formik
      initialValues={initialValues}
      onSubmit={onSubmit}
    >
      <ExampleFormContainer formValues={formValues} />
    </Formik>
  );
};

バリデーション

独自にバリデーションのルールを定義できる
が、ある程度汎用的なルールはあるものを使いたいので Yup が使えるvalidationSchemaの実装例を記載する

yup のバリデーションルール一覧

Formik の validationSchema(prop) に yup で定義したバリデーションルールを入れる感じ

import * as Yup from "yup";

export const validationSchema = Yup.object().shape({
  firstName: Yup.string().required(),
  lastName: Yup.string().required(),
  email: Yup.string().email(),
});

export const ExampleForm = ({
  // ....省略
}: ExampleFormProps): JSX.Element => {
  // ....省略
  return (
    <Formik
      validationSchema={validationSchema}
    >
    // ....省略
  );
};

エラーメッセージの日本語化

このままだとバリデーションに引っ掛かった時のエラーメッセージを日本語化する
setLocal の中のオブジェクトのプロパティはここを参照

import { setLocale } from "yup";
setLocale({
  mixed: {
    required: () => `入力必須項目です。`,
  },
  string: {
    min: ({ min }) => `${min}文字以上で入力して下さい。`,
  },
});

Yup でカスタムバリデーションの定義

test('testの名前', 'エラーメッセージ', (value): boolean => {カスタムバリデーションの定義})
value は直前の型に影響される(string()なら string になる)

例)文字列がYYYY/MM/DDになっているかどうか

const validationSchema = Yup.object().shape({
  date: Yup.string()
    .required()
    .test("checkDateFormat", "日付の形式が間違ってます", (value): boolean => {
      if (!dayjs(value).isValid()) return false;
      const format = "YYYY/MM/DD";
      return dayjs(value, format).format(format) === value;
    }),
});

FieldArray

同一 name の input に配列入れたい時に使うやつ
formik ドキュメントの該当箇所

実装例

type Friend = {
  name: string;
  email: string;
};

export type ExampleFormValues = {
  friends: Friend[];
};

// ...省略
<Field name="friends">
  {({ field }: FieldProps<ExampleFormValues["friends"]>) => (
    <FieldArray name={field.name}>
      {({ remove, push }: ArrayHelpers) => (
        <div>
          {field.value.map((f, i) => (
            <div key={i}>
              <div>
                <InputText
                  name={`friends.${i}.name`}
                  value={f.name}
                  onChange={field.onChange}
                />
                <ErrorMessage name={`friends.${i}.name`} />
              </div>
              <div>
                <InputText
                  name={`friends.${i}.email`}
                  value={f.email}
                  onChange={field.onChange}
                />
                <ErrorMessage name={`friends.${i}.email`} />
              </div>
              <button
                type="button"
                onClick={() => {
                  remove(i);
                }}
              >
                削除
              </button>
            </div>
          ))}
          <button
            type="button"
            onClick={() => {
              push({ name: "", email: "" });
            }}
          >
            友達追加
          </button>
        </div>
      )}
    </FieldArray>
  )}
</Field>;

ArrayHelpers が list を扱う上で便利な helper 用意してくれてる

React Hook Form

インストール

yarn add react-hook-form @hookform/resolvers

基本

比較しやすいように formik でした実装と同じ感じ(presenter, container, <FormProvider />)で実装してみる

presenter

presenter, container, <FormProvider />で分ける場合は子コンポーネントは useFormContext で context を引き回す感じだと思われた

export type ExampleFormValues = {
  firstName: string;
  lastName: string;
  email: string;
};

export type ExampleFormPresenterProps = {
  onSubmit: (values: ExampleFormValues) => void;
};

export const ExampleFormPresenter = ({
  onSubmit,
}: ExampleFormPresenterProps): JSX.Element => {
  const {
    register,
    handleSubmit,
    errors,
  } = useFormContext<ExampleFormValues>();

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="firstName">firstName</label>
        <input name="firstName" ref={register} />
        {errors.firstName && <p>{errors.firstName.message}</p>}
      </div>
      <div>
        <label htmlFor="lastName">lastName</label>
        <input name="lastName" ref={register} />
        {errors.lastName && <p>{errors.lastName.message}</p>}
      </div>
      <div>
        <label htmlFor="email">email</label>
        <input name="email" ref={register} />
        {errors.email && <p>{errors.email.message}</p>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
};

container

例)親コンポーネント側で props を通じて form の値を変えたい時

export type ExampleFormContainerProps = {
  onSubmit: (values: ExampleFormValues) => void;
  formValues?: ExampleFormValues;
};

export const ExampleFormContainer = ({
  onSubmit,
  formValues,
}: ExampleFormContainerProps): JSX.Element => {
  const { reset } = useFormContext<ExampleFormValues>();

  useEffect(() => {
    reset(formValues);
  }, [formValues]);

  return <ExampleFormPresenter onSubmit={onSubmit} />;
};

<FormProvider />

hook が主としての使い方だから form が 1 コンポーネントで完結する場合は使わないけど子コンポーネントに派生する場合に<FormProvider />使う

export type ExampleFormProps = {
  onSubmit: (values: ExampleFormValues) => void;
  formValues?: ExampleFormValues;
};

export const ExampleForm = ({
  onSubmit,
  formValues,
}: ExampleFormProps): JSX.Element => {
  const methods = useForm<ExampleFormValues>();

  return (
    <FormProvider {...methods}>
      <ExampleFormContainer onSubmit={onSubmit} formValues={formValues} />
    </FormProvider>
  );
};

<FormProvider>の子コンポーネントたちには methods が渡されるのでuseFormContextで useForm で使うメソッドが使える

バリデーション

ビルトインのバリデーションがいくつかあるが今回は Yup を使う前提なので割愛
ドキュメントがわかりやすいので見ればイメージは掴めるはず

Yup を使う場合、yupResolverを useForm の引数で resolver として渡してやれば良い

import { yupResolver } from "@hookform/resolvers/yup";

// 前に記載したExampleFormと値が変わってることには触れないで
export const validationSchema = Yup.object().shape({
  name: Yup.string().required(),
  age: Yup.number().required(),
  fruit: Yup.mixed().oneOf(["apple", "banana", "lemon"]).required(),
  category: Yup.mixed().oneOf(["dog", "cat", "rabbit"]).required(),
});

export const ExampleForm = ({
  onSubmit,
  formValues,
}: ExampleFormProps): JSX.Element => {
  const methods = useForm<ExampleFormValues>({
    resolver: yupResolver(validationSchema),
  });

  return (
    <FormProvider {...methods}>
      <ExampleFormContainer onSubmit={onSubmit} formValues={formValues} />
    </FormProvider>
  );
};

useFieldArray

Formik の<FieldArray>の hook 版
これも丁寧にドキュメントに実装方法が記載されているので、ドキュメントを読めば実装イメージが掴めるはず

type Friend = {
  name: string;
  email: string;
};

export type HookFormValues = {
  friends: Friend[];
};

export const HookFormPresenter = ({
  onSubmit,
}: HookFormPresenterProps): JSX.Element => {
  const {
    register,
    handleSubmit,
    errors,
    control,
  } = useFormContext<HookFormValues>();
  const { fields, append, remove } = useFieldArray<Friend>({
    control,
    name: "friends",
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        {fields.map((f, i) => (
          <div key={f.id}>
            <input name={`friends[${i}].name`} ref={register} />
            <input name={`friends[${i}].email`} ref={register} />
          </div>
        ))}
      </div>
      <button
        onClick={() => {
          append({ name: "", email: "" });
        }}
      >
        友達追加
      </button>
      <button
        onClick={() => {
          remove();
        }}
      >
        全削除
      </button>
      <button type="submit">Submit</button>
    </form>
  );
};

appendremoveの他にも配列で扱うために便利そうなメソッドが一通り用意されてた

GitHubで編集を提案

Discussion