LoginSignup
17
9

More than 3 years have passed since last update.

Formikを導入する

Posted at

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に入力した値が入っていることを確認できます。

1.png

配列に対応する

配列データは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.questionsmapでループさせています。
入力エリアは別コンポーネントで管理するようにし、子コンポーネントには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>
  )
}

ただ、これだと入力エリアの切り替えは上手くいきましたが、カテゴリを何回か切り替えてタイプ別ドロップダウンも変更を続けた時、
切り替える前の値が残ってしまっています。
正しくは、選択中のカテゴリだけの動物タイプの値を取得し、選択外は取得させたくないので、一手間加える必要があります。

3.gif

ぐぬぬ、、そうすると…

私はこう実装しました。

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になってくれました。

4.gif

追加、削除

<FieldArray />pushremoveを使うと、配列データに追加・削除などの操作ができます。
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>
      )}
    />
  )
}

renderremovepushを渡し、削除・追加ボタンのonClickで使います。

2.gif

バリデーションを適用する

各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)を指定し、特定のフィールドのデータ(FieldInputPropsFieldMetaPropsFieldHelperProps)が取得できます。

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>
  )
}
17
9
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
17
9