目的
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" }
]
}
};
最終的に作ったもの
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も詳しいわけではないので、もっと良い書き方があれば教えていただけると嬉しいです。