52
35

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のformを簡単に扱おう!

Last updated at Posted at 2019-12-08

はじめに

この記事はPOL AdventCalenderの8日目の記事です。

こんにちは!株式会社POLでフロントエンドエンジニアをしております、橋本ことブリボンと申します!
弊社では、フロントエンドはReactを用いて開発をしております。

みなさんは、Reactでformを作る際、どのように実装しますか?
ライブラリーを使わずに実装する、Formikを使う、redux-formを使うなどなど……
弊社では、Formikを使ってformの実装しております。

また、formのバリデーションを実装するために、 Yup というライブラリーを使っているのですが、Formikの説明だけに注力したいため、詳細な説明は致しません。
別の記事で、Yupを使ったformのバリデーションの解説をしようと思っております。

Formikとは何か?

Formikとは、 Reactでformをより簡単で簡潔に実装できるようにしてくれるライブラリーです。
公式ドキュメント

Formikの特徴として、

  1. formのvalueを管理してくれる
  2. エラーのハンドリングと管理もできる
  3. バリデーションを簡単に設定できる
  4. onChangeやonSubmitなどのイベントハンドラを提供してくれる
  5. idもしくは、name属性を与えることで、どの要素を変更したのかの検知ができる

主に、上記5つの特徴を備えています。
早速、実装の解説に移りましょう。
わかりやすくするために基本編では、ログインフォームのフロントだけの実装を例にして解説していきます。
※reduxは一切出しません。
必要な要素は、メールアドレスとパスワードのinput要素だけです。

Formik[基本編]

Formikとyupをプロジェクトへインストール

yarn add formik yup

これだけですね!

Formikの実装

以下のコードはサンプルコードです。
メールアドレスのname属性はemail
パスワードのname属性はpasswordと設定しています。

Formikでは、 Formikと呼ばれるコンポーネントにいくつかpropsを渡すことで、formの実装をします。

Login.js
import React from "react";
import { Formik } from "formik";

const Login = () => {
  return (
     <Formik
       initialValues={{ email: "", password: "" }}
       onSubmit={values => console.log(values)}
       render={(props) => (
         <form onSubmit={props.handleSubmit}>
           <div>
             <label>email</label>
             <input
               name="email"
               value={props.values.email}
               onChange={props.handleChange}
             />
           </div>
           <div>
             <label>password</label>
             <input
               type="password"
               name="password"
               value={props.values.password}
               onChange={props.handleChange}
             />
           </div>
           <button type="submit">送信</button>
         </form>
       )}
     />
  );
};

export default Login;

initialValues

読んで字の如く、初期値です。
デフォルトでは何も入らないことを想定していますので、空文字列を入れています。
※書きながら思いついたのですが、初期値が必要ない場合、initialValuesを定義する必要はないかもしれません。

仮に、以下のように初期値を渡すと、下の画像の通りになります。


initialValues={{ email: "test@example.com", password: "" }}
Screen Shot 2019-12-08 at 13.52.43.png

onSubmit

こちらもinitialValuesと同じく、名前から役割がわかるのではないでしょうか。
onSumbitイベントが発火した時に、どういう処理を行うか記述する箇所です。
**{}**の中は関数で定義し、引数にはformのvaluesを渡します。

とりあえず、ここではAPIを叩くということもしないので、consoleにデバッグしています。

formにメールアドレスとパスワードを入力し、「送信」ボタンを押すと、入力したものがデバッグされているのが確認できます。
Screen Shot 2019-12-08 at 14.03.47.png

render

Formikの肝であり中身であるrender。
RenderPropsを使っていることもあり、少しわかりづらい実装となっております。

まず、renderはpropsを持ちます。
中身は、values, errors, handleChange, handleSubmit, handleBlur……などなど。
ここで必要なのは、valuesとerrorsとhandleChangeとhandleSubmitです。

valuesには、formに入力したvalueが入り、handleChangeとhandleSubmitはそれぞれ、formのvalueの変更と送信のハンドラーです。

formタグのonSubmitイベントにhandleSubmitを、formの要素のonChangeイベントにhandleChangeを渡すことで、それぞれのハンドラーを使用することができます。
valuesはformの要素に渡さなくても機能します。
初期値を何かしら渡す必要がある時にだけ、渡せば良いです。
※知識不足で申し訳ないですが、idかnameを指定するだけで初期値が入力されるかも。曖昧ですみません(><)

最初の方でも述べましたが、formの要素にはidかname属性、どちらかを指定してください。
私は、name属性を指定する方を好みます。
name属性を指定することで、Formikがどの要素を変更したかを検知できます。

バリデーションの実装

サンプルコードのみを提示します。
画像のようにエラーメッセージが表示されます。

Login.js
import React from "react";
import styled from "styled-components";
import { Formik } from "formik";
import * as Yup from "yup";

const Error = styled.span`
  color: red;
`;

const validation = () =>
  Yup.object().shape({
    email: Yup.string()
      .email("メールアドレスの形式で入力してください")
      .required("必須項目です"),
    password: Yup.string().required("必須項目です")
  });

const Login = () => {
  return (
     <Formik
       initialValues={{ email: "", password: "" }}
       validationSchema={validation()}
       onSubmit={values => console.log(values)}
       render={(props) => (
         <form onSubmit={props.handleSubmit}>
           <div>
             <label>email</label>
             <input
               name="email"
               value={props.values.email}
               onChange={props.handleChange}
             />
             <Error>{props.errors.email}</Error>
           </div>
           <div>
             <label>password</label>
             <input
               type="password"
               name="password"
               value={props.values.password}
               onChange={props.handleChange}
             />
             <Error>{props.errors.password}</Error>
           </div>
           <button type="submit">送信</button>
         </form>
       )}
     />
  );
};

export default Login;
Screen Shot 2019-12-08 at 15.39.08.png

駆け足ではありますが、以上がFormikの基本的な使い方です。
以降ではもう一歩踏み込んで、再利用可能性を高めるためのコンポーネント分割を行っていきます。
そのために、必要なメソッドがあるので順次解説していきます。

Formik[ちょっと応用編]

Formikの悩み

ログインフォームでは、基本的にメールアドレスとパスワードのみの実装で済むので、比較的コードがぐちゃぐちゃになりません。
それでも、上記の例のような書き方をすると、Container componentとPresentational componentが混在しており、やや見辛いコードとなっております。
また、検索フォームや新規登録フォームではどうでしょう?
たくさんのformの要素を使いますよね。

私自身、Formikで可読性や再利用生の高いコードをどう書こうかかなり悩みました。
特に、コンポーネント分割をする方法です。
分割したformのコンポーネントに、handleChangeやvaluesやnameをpropsとして渡せばうまく実装できそうな気がします。
ただ、全コンポーネントでhandleChangeやvaluesを渡すことに、まどろっこしさと抵抗を感じます。

Formikでこれらを簡潔に実装するためにはどうすれば良いでしょう。

検索フォームや新規登録フォームの実装は複雑になりがちなので、これ以降はお問い合わせフォームでの実装を考えます。
なお、以降のお問い合わせフォームは、私がFormikを解説するためだけの目的で作ったものです。
現実には存在しませんので、ご了承を。

お問い合わせフォームの形式

以下のスクショのフォームを作成します。

通常時
Screen Shot 2019-12-08 at 14.28.11.png
エラー時
Screen Shot 2019-12-08 at 16.07.06.png

実装はこんな感じですね。

Contact.js
import React from "react";
import styled from "styled-components";
import { Formik } from "formik";
import * as Yup from "yup";

const Error = styled.span`
  color: red;
`;

const validation = () =>
  Yup.object().shape({
    trigger: Yup.string().required("必須項目です"),
    bothering: Yup.string().required("必須項目です"),
    wishing: Yup.string().required("必須項目です"),
    others: Yup.string().required("必須項目です")
  });


const Contact = () => {
  return (
     <Formik
      initialValues={{ trigger: "", bothering: "", wishing: "", others: "" }}
      validationSchema={validation()}
      onSubmit={values => console.log(values)}
      render={({ errors, handleChange, handleSubmit }) => (
        <form onSubmit={handleSubmit}>
          <div>
            <label>サービスを知ったきっかけは何か</label>
            <input name="trigger" onChange={handleChange} />
            <Error>{errors.bothering}</Error>
          </div>
          <div>
            <label>困っていること</label>
            <input name="bothering" onChange={handleChange} />
            <Error>{errors.trigger}</Error>
          </div>
          <div>
            <label>お願いしたいこと</label>
            <input name="wishing" onChange={handleChange} />
            <Error>{errors.wishing}</Error>
          </div>
          <div>
            <label>その他</label>
            <input name="others" onChange={handleChange} />
            <Error>{errors.others}</Error>
          </div>
          <button type="submit">送信</button>
        </form>
      )}
    />
  );
};

export default Contact;

上のコードを見て何か気づきますか?
name属性だけが違うinputタグが4つもあります。
早速、これらをDRYに書いていきましょう!

コンポーネント分割

name属性とhandleChangeを受け取るInput.jsを作りました。
再利用生を考え、Input.jsはerrorも受け取ってエラーメッセージを返すコンポーネントとしました。

Input.js
import React from "react";
import styled from "styled-components";

const Error = styled.span`
  color: red;
`;

const Input = ({ name, error, handleChange }) => (
  <div>
    <input name={name} onChange={handleChange} />
    <Error>{error}</Error>
  </div>
);

export default Input;

このコンポーネントをimportして使ってみましょう!

Contact.js
import React from "react";
import { Formik } from "formik";
import * as Yup from "yup";
import Input from "./Input";

const validation = () =>
  Yup.object().shape({
    trigger: Yup.string().required("必須項目です"),
    bothering: Yup.string().required("必須項目です"),
    wishing: Yup.string().required("必須項目です"),
    others: Yup.string().required("必須項目です")
  });

const Contact = () => {
  return (
     <Formik
      initialValues={{ trigger: "", bothering: "", wishing: "", others: "" }}
      validationSchema={validation()}
      onSubmit={values => console.log(values)}
      render={({ handleChange, handleSubmit }) => (
        <form onSubmit={handleSubmit}>
          <div>
            <label>サービスを知ったきっかけは何か</label>
            <Input
              name="trigger"
              error={errors.trigger}
              handleChange={handleChange}
            />
          </div>
          <div>
            <label>困っていること</label>
            <Input
              name="bothering"
              error={errors.bothering}
              handleChange={handleChange}
            />
          </div>
          <div>
            <label>お願いしたいこと</label>
            <Input
              name="wishing"
              error={errors.wishing}
              handleChange={handleChange}
            />
          </div>
          <div>
            <label>その他</label>
            <Input
              name="others"
              error={errors.others}
              handleChange={handleChange}
            />
          </div>
          <button type="submit">送信</button>
        </form>
      )}
    />
  );
};

export default Contact;

これだけでも、だいぶ変わりましたね!
4つのformの要素が同じようなデザインとなる場合、Input.jsを変更するだけでよくなりました!

しかし、propsでname, error, handleChangeの3つを送ることは、まだまだ冗長な記述になっているし、Formikからしたら無駄なことをしています。
もっと便利な方法を模索しましょう。

Fieldコンポーネントを使う

どの要素にも必ず必要なhandleChangeをすべての要素でpropsとして渡しているのは、無駄なことだと思いませんか?
また、共通コンポーネントで各々のコンポーネントのerrorを出力する方法があったとしたら……?
FormikのFieldコンポーネントを使うと、handleChangeやerrorをpropsとして送る必要がなくなります。

Input.js
import React from "react";
import styled from "styled-components";
import { Field } from "formik";

const Error = styled.span`
  color: red;
`;

const Input = ({ name }) => (
  <Field name={name}>
    {({ field, form: { errors } }) => (
      <div>
        <input {...field} />
        <Error>{errors[name]}</Error>
      </div>
    )}
  </Field>
);

export default Input;
Contact.js
import React from "react";
import { Formik } from "formik";
import * as Yup from "yup";
import Input from "./Input";

const validation = () =>
  Yup.object().shape({
    trigger: Yup.string().required("必須項目です"),
    bothering: Yup.string().required("必須項目です"),
    wishing: Yup.string().required("必須項目です"),
    others: Yup.string().required("必須項目です")
  });

const Contact = () => {
  return (
    <Formik
      initialValues={{ trigger: "", bothering: "", wishing: "", others: "" }}
      validationSchema={validation()}
      onSubmit={values => console.log(values)}
      render={({ handleSubmit }) => (
        <form onSubmit={handleSubmit}>
          <div>
            <label>サービスを知ったきっかけは何か</label>
            <Input name="trigger" />
          </div>
          <div>
            <label>困っていること</label>
            <Input name="bothering" />
          </div>
          <div>
            <label>お願いしたいこと</label>
            <Input name="wishing" />
          </div>
          <div>
            <label>その他</label>
            <Input name="others" />
          </div>
          <button type="submit">送信</button>
        </form>
      )}
    />
  );
};

export default Contact;

必要なpropsは、nameだけです。
nameをInput.jsに渡せば、どの要素を変更するか、どの要素でエラーが発生しているかが、Formik側で検知してくるれます。

少しだけ解説をすると、field propsにはname, value, onChangeが、formにはvalues, errorsなどの値が入っています。
※デバッグしてみると面白いです。

connectを使う

Fieldコンポーネントと同じことをconnectメソッドで行うことができます。
むしろ、こちらの方が直感的で簡潔な表現ができるのではないでしょうか。

Input.js
import React from "react";
import styled from "styled-components";
import { connect } from "formik";

const Error = styled.span`
  color: red;
`;

const Input = ({ name, formik: { handleChange, errors } }) => {
  return (
    <div>
      <input name={name} onChange={handleChange} />
      <Error>{errors[name]}</Error>
    </div>
  );
};

export default connect(Input);

connectを使うことで、該当のコンポーネントにFormikで使用する変数やメソッドを、propsとして渡すことができます。

終わりに

このように、Formikには様々なことができるようにpropsやメソッド、コンポーネントが用意されています。
ここで紹介したものは、その一部でしかありません。

例えば、setFieldValueというメソッドを使うことで、条件に応じてvalueを変更することができ、より柔軟なformの実装をすることが可能となります。
同じようにsetFieldErrorでは、柔軟にエラーハンドリングを行うことができます。

私自身、まだまだFormikのドキュメントを読み込んでいないので、Formikの真価を発揮できているとは言い難いと思ってます。
今回解説した内容で、疑問に思う方、こういうやり方あるよーというのを知っている方、いやいやここ間違っとるやんと思う方、どしどしコメントお待ちしております!

また、POLでは私たちと一緒にサービスを作っていく仲間を募集しております。
弊社のフロントエンドは特に、まだまだ環境が整いきってません。
ぜひ、お力を貸して欲しいと思っております。
この記事を見てPOLに興味を持った方、アドバイスを持っている方、ぜひぜひお気軽にお声がけください!

52
35
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
52
35

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?