Formikの使い勝手が良すぎるので、備忘録がてらQiitaに残します。
(↓ 分かりやすく説明しているYoutubeもたくさんアップされているので、参考にぜひ
https://youtu.be/FD50LPJ6bjE)
目次
Formikの導入
まずはライブラリをインストールしていきます。
(公式サイトはこちら → https://jaredpalmer.com/formik/)
$ npm i formik
続いて、<form>
タグを <Formik>
へ変更します。
<Formik>
タグに関する公式サイトはこちら → <Formik />
import { Form, Formik } from "formik"
export const App: React.FC<Props> = () => {
return (
// 変更前
<form>
...
</form>
// 変更後
<Formik
initialValues={}
onSubmit={(values, actions) => {
// Submit した時の記述
setTimeout(() => {
alert(JSON.stringify(values, null, 2))
actions.setSubmitting(false)
}, 1000)
}}
>
<Form>
...
</Form>
</Formik>
)
}
また、初期値と型の定義をして下準備を整えておきます。
今回は下のようなデータを扱うことにしていきます。
- カテゴリ「犬」、「猫」、「うさぎ」から選ぶ
- 「ユーザー名」、「メールアドレス」
-
questions
は何個でも入力することができ、追加・削除ができる - カテゴリを選択直後、
question
内の動物タイプがカテゴリごとに描画される - 動物カテゴリ「dogA」「catA」「rabbitA」を選択した時だけ、
questionB
を描画する
type Category = "dog" | "cat" | "rabbit"
// Formで使用する型
export type BaseValues = {
userName: string
email: string
category: Category
questions: Question[]
}
export type Question = {
dogType?: "dogA" | "dogB" | "dogC" | ""
catType?: "catA" | "catB" | "catC" | ""
rabbitType?: "rabbitA" | "rabbitB" | "rabbitC" | ""
questionA: string
questionB?: string
}
初期値を定義し、<Formik>
のinitialValues
にセットする
const initialValues = {
category: "dog" as Category,
userName: "",
email: "",
questions: [
{
questionA: "",
},
],
}
...
<Formik
// 変更後
initialValues={initialValues}
onSubmit={(values, actions) => {}}
>
...
</Formik>
各入力エリアにFormikを適用する
テキストエリア、セレクトボックスをそれぞれ、Formikの<Field>
タグに差し替えます。
バリデーション用のエラーメッセージは、一旦後回しにします。
<Field />
タグに関する公式サイトはこちら → <Field />
name
を正しくセットすることを忘れずに!
import { Form, Formik, Field } from "formik"
export const App: React.FC<Props> = () => {
return (
...
// 変更前
<input type="text" placeholder="名前" />
...
// 変更後
<Field
type="text"
name="userName"
placeholder="名前"
/>
...
// 変更前
<select>
<option selected>犬</option>
<option>猫</option>
<option>うさぎ</option>
</select>
...
// 変更後
<Field as="select" name="category">
<option value="dog">犬</option>
<option value="cat">猫</option>
<option value="rabbit">うさぎ</option>
</Field>
...
)
}
全て差し替えたところでSubmitすると、onSubmit
の引数values
に入力した値が入っていることを確認できます。
配列に対応する
配列データはFormikの<FieldArray />
を使っていきます。
<FieldArray />
タグに関する公式サイトはこちら → <FieldArray />
今回はquestions
に、型Question
のオブジェクトを配列で管理することにしています。
"data": [
{
"dogType": "dogA",
"questionA": "foo",
"questionB": "hoo"
},
{
"dogType": "dogB",
"questionA": "bar",
"questionB": "moo"
},
...
],
import { FieldArray, useFormikContext } from "formik"
export const Questions: React.FC<Props> = () => {
const { values } = useFormikContext<BaseValues>()
return (
<FieldArray
name="questions"
render={() => (
<div>
{values.questions.map((question, index) => (
<div key={index}>
<PetType fieldName={`questions.${index}`} />
<Details fieldName={`questions.${index}`} question={question} />
</div>
))}
</div>
)}
/>
)
}
<FieldArray />
にname
をセットし、render
の中で values.questions
をmap
でループさせています。
入力エリアは別コンポーネントで管理するようにし、子コンポーネントにはname
を渡すようにしました。
1レコードごとのそれぞれの値は、questions.${index}.XXX
で参照できます。
子コンポーネントの方もお見せします。
import { Field } from "formik"
type Props = {
fieldName: string
children?: never
}
export const PetType: React.FC<Props> = ({ fieldName }) => {
return (
<div>
...
<div>タイプ</div>
<Field as="select" name={`${fieldName}.dogType`}>
<option value="">選択してください</option>
<option value="dogA">dogA</option>
<option value="dogB">dogB</option>
<option value="dogC">dogC</option>
</Field>
...
</div>
)
}
特定の入力エリアの値が変わったことを検知し、表示を変更する
今回下の用件で作成していきます。
カテゴリを選択した直後、各カテゴリに対応する入力エリアを描画させる
別コンポーネントで変更された値を検知するのに、useFormikContext()
を使って、フォームの値を取得します。
import { Field, useFormikContext } from "formik"
import { BaseValues } from "./App"
type Props = {
children?: never
fieldName: string
}
export const PetType: React.FC<Props> = ({ fieldName }) => {
const { values } = useFormikContext<BaseValues>()
return (
...
<div>タイプ</div>
<div>
{values.category === "dog" && (
<Field as="select" name={`${fieldName}.dogType`}>
<option value="">選択してください</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</Field>
)}
{values.category === "cat" && (
<Field as="select" name={`${fieldName}.catType`}>
<option value="">選択してください</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</Field>
)}
{values.category === "rabbit" && (
<Field as="select" name={`${fieldName}.rabbitType`}>
<option value="">選択してください</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</Field>
)}
</div>
)
}
ただ、これだと入力エリアの切り替えは上手くいきましたが、カテゴリを何回か切り替えてタイプ別ドロップダウンも変更を続けた時、
切り替える前の値が残ってしまっています。
正しくは、選択中のカテゴリだけの動物タイプの値を取得し、選択外は取得させたくないので、一手間加える必要があります。
ぐぬぬ、、そうすると…
私はこう実装しました。
import { useField, useFormikContext } from "formik"
import { BaseValues, Category } from "./App"
type Props = {
children?: never
}
export const AnimalCategory: React.FC<Props> = () => {
const { values, setFieldValue } = useFormikContext<BaseValues>()
const [field] = useField<Category>("category")
const handleChange = (value: Category): void => {
setFieldValue("category", value)
values.questions.forEach((_q, index) => {
switch (value) {
case "dog":
setFieldValue(`questions.${index}.dogType`, "")
setFieldValue(`questions.${index}.catType`, undefined)
setFieldValue(`questions.${index}.rabbitType`, undefined)
break
case "cat":
setFieldValue(`questions.${index}.dogType`, undefined)
setFieldValue(`questions.${index}.catType`, "")
setFieldValue(`questions.${index}.rabbitType`, undefined)
break
case "rabbit":
setFieldValue(`questions.${index}.dogType`, undefined)
setFieldValue(`questions.${index}.catType`, undefined)
setFieldValue(`questions.${index}.rabbitType`, "")
break
}
})
}
return (
<div>
<div>カテゴリ</div>
<select
{...field}
onChange={(event) => {
handleChange(event.target.value as Category)
}}
onBlur={(event) => {
handleChange(event.target.value as Category)
}}
>
<option value="dog">犬</option>
<option value="cat">猫</option>
<option value="rabbit">うさぎ</option>
</select>
</div>
)
}
setFieldValue
を使うと手動で値の変更などができます。カテゴリが変更された時点、つまり、onChange()
が走った時点で、
該当の動物タイプの値をセットし、且つ、除外するものはundefined
をセットします。
undefined
に指定するとvaluesに含まれないので、Submitすると除外された状態でPOSTなりPUTなりできます。
上記ではswitch
で値をジャッジし、都度setFieldValue
させるようにしています。
(ベストプラクティスご存知の方はぜひアドバイスください…!)
すると、期待通りのvaluesになってくれました。
追加、削除
<FieldArray />
の push
やremove
を使うと、配列データに追加・削除などの操作ができます。
FieldArray Helpersに関する公式サイトはこちら → FieldArray Helpers
export const Questions: React.FC<Props> = () => {
...
return (
<FieldArray
name="questions"
render={({ remove, push }) => (
<div>
{values.questions.map((question, index) => (
<div key={index}>
// 変更後
<button onClick={() => { remove(index) }}>削除</button>
...
</div>
))}
// 変更後
<button
onClick={(e) => {
e.preventDefault()
push({ questionA: "" })
}}
>
追加
</button>
</div>
)}
/>
)
}
render
でremove
、push
を渡し、削除・追加ボタンのonClick
で使います。
バリデーションを適用する
各questionの questionA
だけ入力必須にします。
Submitした時、questionA
が空だったら該当の入力エリア下にエラーメッセージを表示させます。
バリデーションはyupを使おうと思うので、ライブラリインストールから始めることにします。
$ npm i yup
$ npm i @types/yup
バリデーションルールは別ファイルにまとめ、yupの書き方に則って追加していきます。
(yupの詳細は省きます)
import { array, object, string } from "yup"
export const validationSchema = object({
questions: array().of(
object().shape({
questionA: string()
.required("必須です")
.typeError("必須です"),
})
),
})
<Formik />
タグに validationSchema
を追加していきます。
import { validationSchema } from "./validationSchema"
export const App: React.FC<Props> = () => {
return (
<Formik<BaseValues>
initialValues={initialValues}
// 変更後
validationSchema={validationSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2))
actions.setSubmitting(false)
}, 1000)
}}
>
<Form>
...
<button type="submit">
登録する
</button>
</Form>
</Formik>
)
}
入力エリア下にエラーメッセージが表示されるよう、コンポーネントの方も変えていきます。
このまま<Field />
タグを使ってerror
を取得するのも良いですが、今回はuseField()
を使ってエラーを取得するように実装していこうと思います。
useField()
に関する公式サイトはこちら → useField()
useField
の()に useField(該当フィールドのname)
を指定し、特定のフィールドのデータ(FieldInputProps
、FieldMetaProps
、FieldHelperProps
)が取得できます。
import { Field, useField } from "formik"
export const Details: React.FC<Props> = ({ fieldName }) => {
const [field, meta] = useField(`${fieldName}.questionA`)
return (
<div>
<div>Q1.</div>
// 変更前
<Field
as="textarea"
name={`${fieldName}.questionA`}
className="textarea"
/>
// 変更後
<textarea {...field} />
{meta.touched && meta.error ? (
<div className="error">{meta.error}</div>
) : null}
</div>
)
}
感想 & まとめ
Formikは複雑な処理に向いていると思ってます。useFormikContext()
は非常に有用です。
ですが、「ここまでのフォームじゃないんだけど」って時には、別にFormikでなくても良いと思ってます。
react-hook-form
や、別のライブラリ有用だと思っています。
「Formikはかゆいところに手が届きすぎる」「こんなことも出来るんだ!😲」そんな感覚です。
ぜひFormikで感動を味わってください。
コード共有
ここまでのコードは以下の通りです。
(参考例として作ったので、あくまでも本記事のサンプル用として見てください!)
App.tsx
import { Form, Formik } from "formik"
import React from "react"
import { AnimalCategory } from "./AnimalCategory"
import { Questions } from "./Questions"
import { User } from "./User"
import { validationSchema } from "./validationSchema"
export type Category = "dog" | "cat" | "rabbit"
// Formで使用する型
export type BaseValues = {
category: Category
email: string
questions: Question[]
userName: string
}
export type Question = {
catType?: "catA" | "catB" | "catC" | ""
dogType?: "dogA" | "dogB" | "dogC" | ""
questionA: string
questionB?: string
rabbitType?: "rabbitA" | "rabbitB" | "rabbitC" | ""
}
type Props = {
children?: never
}
const initialValues = {
category: "dog" as Category,
userName: "",
email: "",
questions: [
{
questionA: "",
},
],
}
export const App: React.FC<Props> = () => {
return (
<Formik<BaseValues>
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={(values, actions) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2))
actions.setSubmitting(false)
}, 1000)
}}
>
<Form>
<AnimalCategory />
<User />
<Questions />
<button type="submit">
登録する
</button>
</Form>
</Formik>
)
}
AnimalCategory.tsx
import { useField, useFormikContext } from "formik"
import React from "react"
import { BaseValues, Category } from "./App"
type Props = {
children?: never
}
export const AnimalCategory: React.FC<Props> = () => {
const { values, setFieldValue } = useFormikContext<BaseValues>()
const [field] = useField<Category>("category")
const handleChange = (value: Category): void => {
setFieldValue("category", value)
values.questions.forEach((_q, index) => {
switch (value) {
case "dog":
setFieldValue(`questions.${index}.dogType`, "")
setFieldValue(`questions.${index}.catType`, undefined)
setFieldValue(`questions.${index}.rabbitType`, undefined)
break
case "cat":
setFieldValue(`questions.${index}.dogType`, undefined)
setFieldValue(`questions.${index}.catType`, "")
setFieldValue(`questions.${index}.rabbitType`, undefined)
break
case "rabbit":
setFieldValue(`questions.${index}.dogType`, undefined)
setFieldValue(`questions.${index}.catType`, undefined)
setFieldValue(`questions.${index}.rabbitType`, "")
break
}
})
}
return (
<div>
<div>カテゴリ</div>
<select
{...field}
onChange={(event) => {
handleChange(event.target.value as Category)
}}
onBlur={(event) => {
handleChange(event.target.value as Category)
}}
>
<option value="dog">犬</option>
<option value="cat">猫</option>
<option value="rabbit">うさぎ</option>
</select>
</div>
)
}
User.tsx
import React from "react"
import { Field } from "formik"
type Props = {
children?: never
}
export const User: React.FC<Props> = () => {
return (
<div>
<div>ユーザー</div>
<Field
type="text"
name="userName"
placeholder="名前"
/>
<Field
type="text"
name="email"
placeholder="Email"
/>
</div>
)
}
Questions.tsx
import { FieldArray, useFormikContext } from "formik"
import React from "react"
import { BaseValues } from "./App"
import { Details } from "./Details"
import { PetType } from "./PetType"
type Props = {
children?: never
}
export const Questions: React.FC<Props> = () => {
const { values } = useFormikContext<BaseValues>()
return (
<FieldArray
name="questions"
render={({ remove, push }) => (
<div>
{values.questions.map((question, index) => (
<div key={index}>
<button
onClick={() => { remove(index) }}
>
削除
</button>
<PetType fieldName={`questions.${index}`} />
<Details fieldName={`questions.${index}`} question={question} />
</div>
))}
<button
onClick={(e) => {
e.preventDefault()
push({ questionA: "" })
}}
>
追加
</button>
</div>
)}
/>
)
}
PetType.tsx
import { Field, useFormikContext } from "formik"
import React from "react"
import { BaseValues } from "./App"
type Props = {
children?: never
fieldName: string
}
export const PetType: React.FC<Props> = ({ fieldName }) => {
const { values } = useFormikContext<BaseValues>()
return (
<div className="field is-horizontal">
<div>タイプ</div>
<div className="field-body">
<div className="field control">
{values.category === "dog" && (
<Field as="select" name={`${fieldName}.dogType`}>
<option value="">選択してください</option>
<option value="dogA">dogA</option>
<option value="dogB">dogB</option>
<option value="dogC">dogC</option>
</Field>
)}
{values.category === "cat" && (
<Field as="select" name={`${fieldName}.catType`}>
<option value="">選択してください</option>
<option value="catA">catA</option>
<option value="catB">catB</option>
<option value="catC">catC</option>
</Field>
)}
{values.category === "rabbit" && (
<Field as="select" name={`${fieldName}.rabbitType`}>
<option value="">選択してください</option>
<option value="rabbitA">rabbitA</option>
<option value="rabbitB">rabbitB</option>
<option value="rabbitC">rabbitC</option>
</Field>
)}
</div>
</div>
</div>
)
}
Details.tsx
import { Field, useField } from "formik"
import React, { Fragment } from "react"
import { Question } from "./App"
type Props = {
children?: never
fieldName: string
question: Question
}
export const Details: React.FC<Props> = ({ fieldName, question }) => {
const [field, meta] = useField(`${fieldName}.questionA`)
return (
<Fragment>
<div>
<div>Q1.</div>
<textarea
{...field}
className={`textarea
${meta.touched && meta.error && "is-danger"}`}
/>
{meta.touched && meta.error ? (
<div className="help is-danger">{meta.error}</div>
) : null}
{(question.dogType === "dogA" ||
question.catType === "catA" ||
question.rabbitType === "rabbitA") && (
<div>
<div>Q2.</div>
<Field
as="textarea"
name={`${fieldName}.questionB`}
placeholder="DogA, CatA, RabbitA のみ表示する入力エリア"
/>
</div>
)}
</Fragment>
)
}