9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

formikとreact-hook-formの比較-フォームを動的に増やす実装

Last updated at Posted at 2020-09-29

目的

formikとreact-hook-formで動的に項目を増減させる書き方を調べたのでメモ。
主に以下の機能についての検証。

  • formikのFieldArray
  • react-hook-formのuseFieldArray

前提

  • Visual Studio Code 1.49.2
  • react 16.13.1
  • typescript 3.7.5
  • formik 2.1.5
  • react-hook-form 6.8.6
  • 姓、名の2つのデータを複数入力できるようにする。
  • 追加ボタンで姓、名の入力欄を増やす事ができるようにする。

サンプルデータ

formのinputイメージ

const initialValues = {
  listName: "",
  table: {
    rows: [
      { firstName: "sato", lastName: "taro" },
      { firstName: "suzuki", lastName: "jiro" }
    ]
  }
};

最終的に作ったもの

スクリーンショット2020-09-29 19.51.17.png

https://codesandbox.io/s/exciting-chatterjee-n68j3
上半分がformikの実装サンプルで、
下半分がreact-hook-formのサンプル。

ざっくり結論

formikはtypescriptの型がanyになってしまう。componentの定義もツラかった。
react-hook-formなら型定義できるが、hookの使い方が複雑。
typescriptの利用を前提にした場合、個人的な好みはreact-hook-form。

内容

1. formikのFieldArrayについて

form周りはどうしても複雑になりがちですが、
tutorialでとても分かりやすく使い方を定義してくれています。
https://formik.org/docs/tutorial

そんなformik公式ページですが、FieldArrayの解説になると分かりにくい点が多いです。
https://formik.org/docs/api/fieldarray

 import React from 'react';
 import { Formik, Form, Field, FieldArray } from 'formik';

 export const FriendList = () => (
   <div>
     <h1>Friend List</h1>
     <Formik
       initialValues={{ friends: ['jared', 'ian', 'brent'] }}
       // renderはDeprecatedでは?
       render={({ values }) => (
         <Form>
           <FieldArray
             name="friends"
             // ここでもrender
             render={arrayHelpers => (
               <div>
                 {values.friends && values.friends.length > 0 ? (
                   values.friends.map((friend, index) => (
                   // 省略
                   ))
                 )}
                 <div>
                   <button type="submit">Submit</button>
                 </div>
               </div>
             )}
           />
         </Form>
       )}
     />
   </div>
 );

※公式サンプルを一部省略し、コメントを追加

2系ではDeprecatedとなっているはずのrender関数が使われています。
しかも2回も。
そのため、componentが分離できず階層が深くて見通しが悪い実装になっています。

しかし、処理を追っていくとrenderの必要性が薄れてきます。
Formikタグのrenderはvaluesを、FieldArrayタグのrenderはarrayHelpersを取得するためのようですが、公式ページを読み進めると次のような書き方も提示されています。

 import React from 'react';
 import { Formik, Form, Field, FieldArray } from 'formik'
 
 
 export const FriendList = () => (
   <div>
     <h1>Friend List</h1>
     <Formik
       initialValues={{ friends: ['jared', 'ian', 'brent'] }}
       onSubmit={...}
       render={formikProps => (
         <FieldArray
           name="friends"
           component={MyDynamicForm}
         />
       )}
     />
   </div>
 );
 
 
 // In addition to the array helpers, Formik state and helpers
 // (values, touched, setXXX, etc) are provided through a `form`
 // prop
 export const MyDynamicForm = ({
   move, swap, push, insert, unshift, pop, form
 }) => (
  <Form>
   {/**  whatever you need to do */}
  </Form>
 );

※公式サンプルをそのまま貼り付け

ポイントは以下の3点です。

  • FieldArrayのpropsにcomponentを設定できる。
  • 上記のcomponentには引数でhelpersを受け取る。
  • helpers.form.valuesとたどればvaluesが取得できる

どうやらrenderで取得せずともvaluesやhelperを取得できるらしい。
ということで、FieldArrayでcomponentを使って実装してみました。

2. formikのFieldArrayとcomponentを使った実装

ただ、実装してみると、思ったほどうまくいきません。
typescript周りで面倒な事が起こります。

実装のコメントにも書いていますが、componentに渡すFCの定義がエラーになってしまいます。

// 親コンポーネント
const FormikPart: React.FC = () => {
  return (
    <>
      <h2>Formik array</h2>
      <Formik
        initialValues={initialValues}
        onSubmit={(values, helper) => {
          console.log(values);
        }}
      >
        <Form>
          <FieldArray name="table.rows" component={ArrayInput} />
          <button type="submit">submit</button>
        </Form>
      </Formik>
    </>
  );
};

// 子コンポーネント
// <void | FieldArrayRenderProps>を受け取れないとerrorになる
const ArrayInput: React.FC<void | FieldArrayRenderProps> = (props) => {
  // propsの型がReact.PropsWithChildren<void | FieldArrayRenderProps>になってしまうので
  // as で型を定義する
  const { form, name, push } = props as React.PropsWithChildren<
    FieldArrayRenderProps
  >;

  // name = table.rows
  // getInを使うと"table.rows"をハードコードする場所が減らせる
  // table.rowsの配列の値は取得できるが、型はany
  const array = getIn(form.values, name);

  // 型がanyなのでチェック等を書く必要がある
  if (!(Array.isArray(array) && array.length > 0)) return null;

  const list = array.map((val, index) => (
    <div key={index}>
      <Field name={`${name}[${index}].firstName`} type="text" />
      <Field name={`${name}[${index}].lastName`} type="text" />
    </div>
  ));
  return (
    <>
      {list}
      <button
        type="button"
        // pushの引数も型もanyなので型チェックはされない
        onClick={() => push({ firstName: "tanaka", lastName: "saburo" })}
      >
        add at last
      </button>
    </>
  );
};

コード全体が見たい方はこちら
https://codesandbox.io/s/exciting-chatterjee-n68j3?file=/src/components/FormikPart.tsx

FieldArrayRenderPropsはformikから提供されているFieldArrayの引数の型です。
本来ならReact.FC<FieldArrayRenderProps>のようにFCを定義したいのですが、
propsのcomponentの型定義がReact.ComponentType<T | void>となっておりvoidの指定をしないとtypescriptから怒られてしまいます。
React.ComponentType<void>という定義にどんな意味があるのかはよくわかりませんが...仕方なく上記のように型を定義し、引数のpropsはasで型を再度定義します。

また、formから取得した入力値も型がanyになってしまい、firstNameやlastNameといったオブジェクト構造のインテリセンスも効かないため、気をつけて実装する必要があります。

こうしたtypescriptの型定義の難しさを避けるために、別のライブラリも試してみることにしました。

3. react-hook-formのuseFieldArrayを使った実装

typescriptでよりシンプルに書くために、新しいライブラリであるreact-hook-formにトライしました。

react-hook-formのドキュメントはとてもオシャレです。
formikのFieldArrayに相当するのはuseFieldArrayというhookです。

必要な関数は最初からhookで提供されているので、renderの引数やcomponentの分離といったことは考えなくて大丈夫でした。

// 親コンポーネント
const ReactHookFormPart: React.FC = () => {
  const methods = useForm<FormInputs>({ defaultValues });
  const onSubmit = (data: FormInputs) => console.log(data);

  return (
    <>
      <h2>React Hook Form array</h2>
      <FormProvider {...methods}>
        <form onSubmit={methods.handleSubmit(onSubmit)}>
          <ArrayInput />
          <button type="submit">submit</button>
        </form>
      </FormProvider>
    </>
  );
};

// 子コンポーネント
const name = "table.rows";
const ArrayInput: React.FC = () => {
  // hook利用時に型を指定する必要はあるが、型定義ができる
  const { control, register } = useFormContext<FormInputs>();
  // useFieldArrayでは配列の要素の型を指定する
  const { fields, append } = useFieldArray<TableRows>({
    control,
    // name(table.rows)を指定することで,fieldsにtable.rowsの値が入る
    name
  });

  const list = fields.map((item, index) => (
    <div key={item.id}>
      <input
        name={`${name}[${index}].firstName`}
        // registerはregister()としてmap内で実行する必要がある
        ref={register()}
        // itemも型定義されている
        defaultValue={item.firstName}
      />
      <input
        name={`${name}[${index}].lastName`}
        ref={register()}
        defaultValue={item.lastName}
      />
    </div>
  ));

  return (
    <>
      {list}
      <button
        type="button"
        //  appendの引数も型定義されている
        onClick={() => append({ firstName: "tanaka", lastName: "saburo" })}
      >
        add at last
      </button>
    </>
  );
};

export default ReactHookFormPart;

コード全体が見たい方はこちら
https://codesandbox.io/s/exciting-chatterjee-n68j3?file=/src/components/ReactHookFormPart.tsx

子コンポーネントでhooksの利用時に方を指定する必要がありますが、型定義済みの入力値を取得できます。
配列を操作する関数にも型定義がされているため、要素を追加する際にオブジェクトの構造を間違えてしまうこともなさそうです。

しかし、registerの使い方やuseForm、useFormContext、useFieldArrayのhook3段活用は、正直理解するのに時間がかかりました(使い方が正しいのかちょっと自信がありません...)。
全体的に簡単そうに見えて意外と難しい、というのがreact-hook-formの印象です。

終わり

formik
難しそう... → 意外とカンタンかも → typescriptがerrorとかanyとか

react-hook-form
なんかオシャレ → なんか難しい → なんか難しい(けどtypescriptエラーはない)

という印象でした。個人的にはtypescriptで書けることを重視しているのでひとまずreact-hook-formを使おうかと考えています。
ただ、formikもシンプルなフォームであればシンプルに書けるし、FieldArrayもrenderなしで書けるようにアップデートされて上記の悩みがなくなっていくかもしれません。
そもそもformikもreact-hook-formも詳しいわけではないので、もっと良い書き方があれば教えていただけると嬉しいです。

9
3
0

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
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?