※このブログではサーバー運用、技術の検証等の費用のため広告をいれています。
記事が見づらいなどの問題がありましたらContactからお知らせください。

次のページ>
【React + TypeScript】formikで高機能フォームを作成|Part2

【React + TypeScript】formikで高機能フォームを作成|Part1

react web開発 javascript フロント開発 TypeScript バリデーション yup

投稿日:2021年6月6日

このエントリーをはてなブックマークに追加
formikというパッケージについて、このパッケージの機能の中身を解説しつつ、使い方について詳しく解説していきます。Part1では一般的なユースケースではなく、formikの動作について理解を深める部分に焦点をあてています。

はじめに

この記事について

Webフロントの開発ではページ遷移のしないフォームや高機能なフォームの作成をすることは多々あります。しかし、そのようなフォームの実装は時間はかかるしコードは煩雑になりがちです。そんな時にはformikというパッケージを使うことをオススメします。

これからformikというパッケージについて、このパッケージの機能の中身を解説しつつ、使い方について詳しく解説していきます。Part1では一般的なユースケースではなく、formikの動作について理解を深める部分に焦点をあてています。

formik

formikはフォームを効率的に管理するパッケージです。formikを使って作成したフォームでは以下のようなことができます。

  • フォームの状態の値のやり取り
  • 値のバリデーションとエラーメッセージの管理
  • 送信処理の管理

これらの管理を一つの場所で管理するとリファクタリングやテストの管理がしやすくなります。

環境について

この記事のコードは以下の環境で確認されました。

  • node v10.19.0
  • npm v6.14.4
  • react v17.0.2
  • bashのターミナル

またこの記事では既にReactのプロジェクトは作成済のものとします。

参考URL


実践

インストール

まずはReactのプロジェクトにformikをインストールします。

ターミナル
$ npm install --save formik

簡単なフォームの作成

formikを使い、入力と送信処理(偽)をする簡単なフォームを作成してみます。

SignupForm.tsx
import React from "react";
import { useFormik } from "formik";


/**
 * フォームの型
 */
type FormValueType = {
    email: string,
    firstName: string,
    lastName: string
}

/**
     * 送信処理
     * @param values 
     */
const onSubmit = (values: FormValueType) => {
        alert(JSON.stringify(values, null, 2));
}

/**
 * 登録フォーム
 */
const SignupForm = () => {
    /**
     * フォームの定義
     */
    const formik = useFormik<FormValueType>({
        initialValues: {email : "", firstName:"", lastName:""},
        onSubmit: onSubmit
    });

    return (
        <div>
            <form onSubmit={formik.handleSubmit}>
                <label htmlFor="email">メールアドレス</label>
                <input id="email" name="email" type="email" onChange={formik.handleChange} value={formik.values.email}></input>
                <label htmlFor="lastName">姓</label>
                <input id="lastName" name="lastName" type="text" onChange={formik.handleChange} value={formik.values.lastName}></input>
                <label htmlFor="firstName">名</label>
                <input id="firstName" name="firstName" type="text" onChange={formik.handleChange} value={formik.values.firstName}></input>
                <button type="submit" onSubmit={() => {formik.handleSubmit();}}>送信</button>
            </form>
        </div>
    )
}

export default SignupForm;

FormValueTypeは作成するフォームの入力値の型になります。formikの利用に型の定義は必須ではありませんが、メンテンナンス性などのためにちゃんと定義しておくことをオススメします。

useFormik<T>()はformikで実装されているReact Hooksです。formikのメインである<Formik>コンポーネントは内部でuseFormik<T>()を使用していて、基本的にはそちらを使えば良いのですが、今回はサンプルで理解を深めるためにuseFormik<T>()をよびだしました。

実際に動かすと想定通りにフォームを管理できていることがわかります。

▲フォームの動きの確認

フォームのnameをFormValueTypeのプロパティ名と同じ値にしていることに注意してください。以下のような処理と同等だと考えてください。

こんなイメージ
const [values, setValues] = React.useState<FormValueType>({email:"",firstName:"",lastName:""});

const handleChange = (e: React.ChangeEvent<HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement>) => {
        setValues({
            ...values,
            [e.target.name]: e.target.value
        });
    }

そのため対象の<input>name属性を対象のプロパティと違うものにすると、値をうまくセットできないので注意しましょう。

バリデーションを追加する

次に値のバリデーションを追加してみましょう。

SignupForm.tsx
import React from "react";
import { useFormik } from "formik";


/**
 * フォームの型
 */
type FormValueType = {
    email: string,
    firstName: string,
    lastName: string
}

/**
 * フォームのエラーの型定義
 */
type FormErrorType = {
    [P in keyof FormValueType]?: string;
};

/**
 * バリデーション用の関数
 * @param values 
 */
const validate = (values: FormValueType):FormErrorType => {
    const errors: FormErrorType = {};
    
    if(!values.firstName){
        errors.firstName = "このフィールドは必須です"
    }else if(values.firstName.length > 15){
        errors.firstName = "15文字以下で入力してください"
    }

    if(!values.lastName){
        errors.lastName = "このフィールドは必須です"
    }else if(values.lastName.length > 15){
        errors.lastName = "15文字以下で入力してください"
    }

    if(!values.email){
        errors.email = "このフィールドは必須です"
    }else if(!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)){
        errors.email = "正しいメールアドレスを入力してください"
    }

    return errors;
}

/**
     * 送信処理
     * @param values 
     */
 const onSubmit = (values: FormValueType) => {
    alert(JSON.stringify(values, null, 2));
}

/**
 * 登録フォーム
 */
const SignupForm = () => {

    /**
     * フォームの定義
     */
    const formik = useFormik<FormValueType>({
        initialValues: {email : "", firstName:"", lastName:""},
        validate: validate,
        onSubmit: onSubmit
    });

    return (
        <div>
            <form onSubmit={formik.handleSubmit}>
                <label htmlFor="email">メールアドレス</label>
                <input id="email" name="email" type="email" onChange={formik.handleChange} value={formik.values.email}></input>
                {formik.errors.email ? <div>{formik.errors.email}</div> : null}
                <label htmlFor="lastName">姓</label>
                <input id="lastName" name="lastName" type="text" onChange={formik.handleChange} value={formik.values.lastName}></input>
                {formik.errors.lastName ? <div>{formik.errors.lastName}</div> : null}
                <label htmlFor="firstName">名</label>
                <input id="firstName" name="firstName" type="text" onChange={formik.handleChange} value={formik.values.firstName}></input>
                {formik.errors.firstName ? <div>{formik.errors.firstName}</div> : null}
                <button type="submit" onSubmit={() => {formik.handleSubmit();}}>送信</button>
            </form>
        </div>
    )
}

export default SignupForm;

フォームの入力値のバリデーションには各入力値に対してのエラーメッセージを入れたオブジェクトが必要になります。FormErrorTypeはそのオブジェクトの型定義になります。これはMapped Typeの文法を使い、FormValueTypeと同じプロパティ名で型がstringのプロパティを持ったオブジェクトの型を定義しています。エラーオブジェクトはエラー発生時にのみプロパティを定義します。そのため、すべてのプロパティはオプションにしてあります。

validate()はバリデーションを行う関数になります。この関数をuseFormik<T>()に渡す引数オブジェクトのvalidateプロパティとして渡します。関数の型はフォームの値を引数に、エラーオブジェクトの型を返り値にします。初期化時に空にしたerrorsオブジェクトに対して、フォームの値の問題が見つかるたびにプロパティを追加していきます。返り値が空でない場合(どこかでバリデーション結果がfalseになった場合)には、handleSubmit()を呼び出してもonSubmit()が呼び出されることはありません。

TSX内では、バリデーションで見つかったエラーをformik.errorsオブジェクトから呼び出しています。Formikはデフォルトではフォームの入力値が変更されるたびにvalidateに渡した関数が呼び出されます。

実行すると下のようにバリデーションの動作が確認できます。

▲バリデーションが動作している

フォームのバリデーションは問題なく動作していますね!!

しかし、ここで気になる点があります。一つのフィールドが入力された時点ですべてのバリデーションのエラーが表示されてしまう点がUI・UX的にあまりよくありませんね。

次にこの部分を修正していきましょう。

Visited Fields

入力済の要素のみバリデーションエラーを表示するようにしてみます。

formikにはフォームの各フィールドごとに入力状態を保持するboolean型touchedという変数が用意されています。この変数はformikにデフォルトで実装されているhandleBlur()関数を呼び出すことで、自動的にtrueになります。
どのフィールドのtouchedtrueになるのかは、対象HTML要素のname属性でコントロールされます。この部分はhandleChange()と同じように考えればOKですね。

では実装してみましょう。

SignupForm.tsx
import React from "react";
import { useFormik } from "formik";


/**
 * フォームの型
 */
type FormValueType = {
    email: string,
    firstName: string,
    lastName: string
}

/**
 * フォームのエラーの型定義
 */
type FormErrorType = {
    [P in keyof FormValueType]?: string;
};

/**
 * バリデーション用の関数
 * @param values 
 */
const validate = (values: FormValueType):FormErrorType => {
    const errors: FormErrorType = {};
    
    if(!values.firstName){
        errors.firstName = "このフィールドは必須です"
    }else if(values.firstName.length > 15){
        errors.firstName = "15文字以下で入力してください"
    }

    if(!values.lastName){
        errors.lastName = "このフィールドは必須です"
    }else if(values.lastName.length > 15){
        errors.lastName = "15文字以下で入力してください"
    }

    if(!values.email){
        errors.email = "このフィールドは必須です"
    }else if(!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)){
        errors.email = "正しいメールアドレスを入力してください"
    }

    return errors;
}

/**
     * 送信処理
     * @param values 
     */
 const onSubmit = (values: FormValueType) => {
    alert(JSON.stringify(values, null, 2));
}

/**
 * 登録フォーム
 */
const SignupForm = () => {

    /**
     * フォームの定義
     */
    const formik = useFormik<FormValueType>({
        initialValues: {email : "", firstName:"", lastName:""},
        validate: validate,
        onSubmit: onSubmit
    });

    return (
        <div>
            <form onSubmit={formik.handleSubmit}>
                <label htmlFor="email">メールアドレス</label>
                <input id="email" name="email" type="email" onChange={formik.handleChange} onBlur={formik.handleBlur}  value={formik.values.email}></input>
                {formik.touched.email && formik.errors.email ? (
         <div>{formik.errors.email}</div>
       ) : null}
                <label htmlFor="lastName">姓</label>
                <input id="lastName" name="lastName" type="text" onChange={formik.handleChange} onBlur={formik.handleBlur} value={formik.values.lastName}></input>
                {formik.touched.lastName && formik.errors.lastName ? (
         <div>{formik.errors.lastName}</div>
       ) : null}
                <label htmlFor="firstName">名</label>
                <input id="firstName" name="firstName" type="text" onChange={formik.handleChange} onBlur={formik.handleBlur} value={formik.values.firstName}></input>
                {formik.touched.firstName && formik.errors.firstName ? (
         <div>{formik.errors.firstName}</div>
       ) : null}
                <button type="submit" onSubmit={() => {formik.handleSubmit();}}>送信</button>
            </form>
        </div>
    )
}

export default SignupForm;

ここではそれぞれの入力要素のonBlurhandleBlur()を渡しています。blurイベントは要素がフォーカスを失ったときに発生します。ユーザーが入力要素からフォーカスを外すのは基本的に『次の入力要素に進む』、または『送信ボタンを押す時』なのでタイミングとしては良さそうですね。

エラーの表示には、前の例から対象フィールドのtouchedtrueであるという条件を追加しました。

動作を確認してみましょう。

▲エラーメッセージの表示タイミングの制御

きちんと入力が完了されるまでエラーメッセージの表示を制限できていることが確認できました!

【補足】yupを使って複雑なバリデーション

ここまででカスタムのバリデーションの実装方法は十分理解できました。

ここまではシンプルなバリデーションのみでしたが、本格的なバリデーションをする場合には、バリデーション関数が非常に複雑になることが予測されます。そういう場合にはyupなどのパッケージとvalidationSchemaの機能を使ってみましょう。

ターミナル
$ npm install --save yup

公式Docによるとyupを正しく動作させるためにtsconfig.jsonstrictの設定が必要になるようです。

tsconfig.json
{
    [...]

    "strict": false,
    "strictFunctionTypes": false,
    "strictNullChecks": true,

     [...]
}
SignupForm.tsx
import React from "react";
import { useFormik } from "formik";
import * as Yup from "yup";


/**
 * フォームの型
 */
type FormValueType = {
    email: string,
    firstName: string,
    lastName: string
};

/**
 * バリデーションスキーマ
 */
const validationSchema = Yup.object({
    firstName: Yup.string().max(15, "15文字以下で入力してください").required("このフィールドは必須です"),
    lastName: Yup.string().max(15, "15文字以下で入力してください").required("このフィールドは必須です"),
    email: Yup.string().email("正しいメールアドレスを入力してください").required("このフィールドは必須です")
});

/**
     * 送信処理
     * @param values 
     */
 const onSubmit = (values: FormValueType) => {
    alert(JSON.stringify(values, null, 2));
}

/**
 * 登録フォーム
 */
const SignupForm = () => {

    /**
     * フォームの定義
     */
    const formik = useFormik<FormValueType>({
        initialValues: {email : "", firstName:"", lastName:""},
        validationSchema: validationSchema,
        onSubmit: onSubmit
    });

    return (
        <div>
            <form onSubmit={formik.handleSubmit}>
                <label htmlFor="email">メールアドレス</label>
                <input id="email" name="email" type="email" onChange={formik.handleChange} onBlur={formik.handleBlur}  value={formik.values.email}></input>
                {formik.touched.email && formik.errors.email ? (
         <div>{formik.errors.email}</div>
       ) : null}
                <label htmlFor="lastName">姓</label>
                <input id="lastName" name="lastName" type="text" onChange={formik.handleChange} onBlur={formik.handleBlur} value={formik.values.lastName}></input>
                {formik.touched.lastName && formik.errors.lastName ? (
         <div>{formik.errors.lastName}</div>
       ) : null}
                <label htmlFor="firstName">名</label>
                <input id="firstName" name="firstName" type="text" onChange={formik.handleChange} onBlur={formik.handleBlur} value={formik.values.firstName}></input>
                {formik.touched.firstName && formik.errors.firstName ? (
         <div>{formik.errors.firstName}</div>
       ) : null}
                <button type="submit" onSubmit={() => {formik.handleSubmit();}}>送信</button>
            </form>
        </div>
    )
}

export default SignupForm;

大分シンプルな記述にすることができました。
yupに詳しい使い方についてはこの記事の内容から外れるので、ここでは説明は省きます。興味があれば調べてみてください。

このエントリーをはてなブックマークに追加

次のページ>
【React + TypeScript】formikで高機能フォームを作成|Part2

関連記事

記事へのコメント