投稿日:2021年6月6日
formikというパッケージについて、このパッケージの機能の中身を解説しつつ、使い方について詳しく解説していきます。Part1では一般的なユースケースではなく、formikの動作について理解を深める部分に焦点をあてています。
Webフロントの開発ではページ遷移のしないフォームや高機能なフォームの作成をすることは多々あります。しかし、そのようなフォームの実装は時間はかかるしコードは煩雑になりがちです。そんな時にはformikというパッケージを使うことをオススメします。
これからformikというパッケージについて、このパッケージの機能の中身を解説しつつ、使い方について詳しく解説していきます。Part1では一般的なユースケースではなく、formikの動作について理解を深める部分に焦点をあてています。
formikはフォームを効率的に管理するパッケージです。formikを使って作成したフォームでは以下のようなことができます。
これらの管理を一つの場所で管理するとリファクタリングやテストの管理がしやすくなります。
この記事のコードは以下の環境で確認されました。
またこの記事では既にReactのプロジェクトは作成済のものとします。
まずはReactのプロジェクトにformikをインストールします。
$ npm install --save formik
formikを使い、入力と送信処理(偽)をする簡単なフォームを作成してみます。
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属性を対象のプロパティと違うものにすると、値をうまくセットできないので注意しましょう。
次に値のバリデーションを追加してみましょう。
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的にあまりよくありませんね。
次にこの部分を修正していきましょう。
入力済の要素のみバリデーションエラーを表示するようにしてみます。
formikにはフォームの各フィールドごとに入力状態を保持するboolean型のtouchedという変数が用意されています。この変数はformikにデフォルトで実装されているhandleBlur()関数を呼び出すことで、自動的にtrueになります。
どのフィールドのtouchedがtrueになるのかは、対象HTML要素のname属性でコントロールされます。この部分はhandleChange()と同じように考えればOKですね。
では実装してみましょう。
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;
きちんと入力が完了されるまでエラーメッセージの表示を制限できていることが確認できました!
ここまででカスタムのバリデーションの実装方法は十分理解できました。
ここまではシンプルなバリデーションのみでしたが、本格的なバリデーションをする場合には、バリデーション関数が非常に複雑になることが予測されます。そういう場合にはyupなどのパッケージとvalidationSchemaの機能を使ってみましょう。
$ npm install --save yup
公式Docによるとyupを正しく動作させるためにtsconfig.jsonのstrictの設定が必要になるようです。
{
[...]
"strict": false,
"strictFunctionTypes": false,
"strictNullChecks": true,
[...]
}
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に詳しい使い方についてはこの記事の内容から外れるので、ここでは説明は省きます。興味があれば調べてみてください。