この記事を読むと幸せになれそうな人
Controlled Component (React.useState
や Redux を使う方法)で大きめのフォームを構築するとき、UIライブラリを使ったり、バリデーションを実装したりすると、ボイラープレートコードが多すぎて辟易してしまうと思います。
そんな人には、この React Final Form がオススメかもしれません。
続き物です
- #01『かんたんにフォームを構築する』
- #02『Render Props を使って、かんたんにバリデーションを実装する』 ←今ここ
- #03『かんたんに2回クリックの防止とサーバー側バリデーションを実装する』
0. emailアドレス欄を追加
まずは、前回に続いて、emailアドレスの入力欄を追加します。(バリデーションの題材としてはこっちの方が手っ取り早いので)
const waitMs = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
const initialValues = {
name: "aa",
+ email: ""
};
type ValuesType = typeof initialValues;
const ReactFinalFormPage: NextPage = () => {
type HandleSubmit = FormProps<ValuesType, ValuesType>["onSubmit"];
const handleSubmit: HandleSubmit = async (s) => {
await waitMs(1000);
window.alert(`JSON: ${JSON.stringify(s)}`);
// 例) JSON: {"name":"山田太郎"}
};
return (
<Form
onSubmit={handleSubmit}
initialValues={initialValues}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<Field name="name" component="input" />
+ <Field name="email" component="input" />
<button>送信</button>
</form>
)}
/>
);
};
export default ReactFinalFormPage;
1. Field
で、 Render Props を使って書き換え
Fieldの機能はそのままで、 component
Prop を使わずに、 Render Prop を使った書き方に変えてみましょう。後々バリデーションメッセージを表示したり、Material UI のような UI ライブラリを使う際ためには、おそらくこの書き方に変える必要があるでしょう。
- <Field name="email" component="input" />
+ <Field<string> name="email" render={({ input }) => <input {...input} />} />
<Field<string> ...>
の部分は、わかりにくいですが、Fieldコンポーネントに、型引数として、string
を渡せているみたいです。こんな変なこともできるんですね。
Render Prop の引数は、Field Render Props 型になります。 関数の引数オブジェクトの input
プロパティはオブジェクトになっています。基本的には、この内容を全て <input/>
要素に渡すことになっています。スプレッド構文を使ってまとめて渡しましょう。
2. バリデータの追加
それでは、お待ちかねのバリデーションの実装に移ります。
次の const 宣言をトップレベルに書きましょう。
const validateEmail: FieldValidator<string> = (value): string[] | undefined => {
if (!value || value.length === 0) return ["必須要素です"];
const emailRegexp =
/^[a-zA-Z0-9_+-]+(.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/;
if (!emailRegexp.test(value))
return ["Eメールアドレスのフォーマットに適していません"];
return undefined;
};
React Final Form では、Fieldに追加するバリデーション関数は FieldValidator<T>
型になります。
返り値としては、エラーがない時には undefined
を返し、エラーの時には、エラーを表す何らかの値を返しましょう。
実は、async 関数もOKです。(今回は使いません)
(今回は、string[]
型の値に決めていますが、返り値の型は元々決まっていないので、自分で決めましょう。)
メールアドレスの正規表現は下のページを参考にしています。
これを、Fieldのコンポーネントに実装していきましょう。
<Field<string>
name="email"
validate={validateEmail}
render={({ input, meta }) => (
<div>
<input {...input} />
<span>
{meta.error &&
meta.touched &&
meta.error.map((s: string) => <small key={s}>{s}</small>)}
</span>
</div>
)}
/>
まず、validate
Propに先ほどのバリデーション関数を渡します。
すると、Render Props の meta.error
にバリデーション関数が返したエラーが入って渡されてきます。
meta.error && meta.touched &&
を付けていますが、これは、
-
error
がundefined
なら何も表示しない。 - フィールドが、フォーカスを外されたことがない場合にも、何も表示しない。
-
meta.touched
は、一度フォーカスされたあとフォーカスが外れたらtrue
になるフラグ
-
ことを表しています。
Field Render Props のドキュメントを見ると、他にも様々なフラグ(meta.xxx
)があります。それらを使うと、エラーメッセージの表示の切り替えのタイミングを様々に変えたり、初期値から変更されたフィールドだけを強調したりすることもできます。
3. バリデーションが成功していない場合にボタンをdisabled
にする
ついでに、バリデーションが成功していない場合には、ボタンを disabled
にしてしまいましょう。
こうしておかないと、入力値が間違っていても送信できてしまいますからね。
<Form
onSubmit={handleSubmit}
initialValues={initialValues}
- render={({ handleSubmit }) => (
+ render={({ handleSubmit, valid }) => (
<form onSubmit={handleSubmit}>
<Field name="name" component="input" />
<Field<string>
name="email"
validate={validateEmail}
render={({ input, meta }) => (
<div>
<input {...input} />
<span>
{meta.error &&
meta.touched &&
meta.error.map((s: string) => <small key={s}>{s}</small>)}
</span>
</div>
)}
/>
- <button>送信</button>
+ <button disabled={!valid}>送信</button>
</form>
)}
/>
Form の Render Props (ドキュメント : FormRenderProps ) には、valid
Prop があります。中のフィールドのバリデーションが全て成功している時にこの値が true
になります。
これによって、バリデーションエラーが出ているときに、ボタンを押せなくすることができました。
4. エラーメッセージをコンポーネント化
エラーメッセージの記述が長ったらしくなっていたので、コンポーネントに切り出して、記述を楽にします。
<Field<string>
name="email"
validate={validateEmail}
render={({ input, meta }) => (
<div>
<input {...input} />
- <span>
- {meta.error &&
- meta.touched &&
- meta.error.map((s: string) => <small key={s}>{s}</small>)}
- </span>
+ <ErrorMsg hidden={meta.touched} msgs={meta.error}/>
</div>
)}
/>
type ErrorMsgProps = { hidden?: boolean; msgs: string[] };
const ErrorMsg: React.FC<ErrorMsgProps> = ({ hidden, msgs }) => (
<span>{msgs && hidden && msgs.map((s) => <small key={s}>{s}</small>)}</span>
);
5. 名前にもバリデーションをつける
名前にも、文字列が空のときにエラーを返すバリデータを追加します。
コンポーネント化したので、コードも短くてラクラクですね。
const validateRequiredString: FieldValidator<string> = (
value
): string[] | undefined => {
if (!value || value.length === 0) return ["必須要素です"];
return undefined;
};
<form onSubmit={handleSubmit}>
- <Field name="name" component="input" />
+ <Field<string>
+ name="name"
+ validate={validateRequiredString}
+ render={({ input, meta }) => (
+ <div>
+ <input {...input} />
+ <ErrorMsg hidden={meta.touched} msgs={meta.error} />
+ </div>
+ )}
+ />
<Field<string>
name="email"
validate={validateEmail}
render={({ input, meta }) => (
<div>
<input {...input} />
<ErrorMsg hidden={meta.touched} msgs={meta.error} />
</div>
)}
/>
<button disabled={!valid}>送信</button>
</form>
)}
/>
);
};
export default ReactFinalFormPage;
まとめ
いかがだったでしょうか?
Filed の Render Props を使えば、一貫した方法で柔軟にフィールドを制御したり、バリデーションを実装したりできることがわかったと思います。
送信ボタンの活/不活も、同様のインターフェースで制御できましたね。
次回以降はまだ未定です。
コード全容
import { FieldValidator } from "final-form";
import { NextPage } from "next";
import { Field, Form, FormProps } from "react-final-form";
const waitMs = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
const initialValues = {
name: "aa",
email: "",
};
type ValuesType = typeof initialValues;
const validateRequiredString: FieldValidator<string> = (
value
): string[] | undefined => {
if (!value || value.length === 0) return ["必須要素です"];
return undefined;
};
const validateEmail: FieldValidator<string> = (value): string[] | undefined => {
if (!value || value.length === 0) return ["必須要素です"];
// from https://www.javadrive.jp/regex-basic/sample/index13.html#section1
const emailRegexp =
/^[a-zA-Z0-9_+-]+(.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/;
if (!emailRegexp.test(value))
return ["Eメールアドレスのフォーマットに適していません"];
return undefined;
};
type ErrorMsgProps = { hidden?: boolean; msgs: string[] };
const ErrorMsg: React.FC<ErrorMsgProps> = ({ hidden, msgs }) => (
<span>{msgs && hidden && msgs.map((s) => <small key={s}>{s}</small>)}</span>
);
const ReactFinalFormPage: NextPage = () => {
type HandleSubmit = FormProps<ValuesType, ValuesType>["onSubmit"];
const handleSubmit: HandleSubmit = async (s) => {
await waitMs(1000);
window.alert(`JSON: ${JSON.stringify(s)}`);
};
return (
<Form
onSubmit={handleSubmit}
initialValues={initialValues}
render={({ handleSubmit, valid }) => (
<form onSubmit={handleSubmit}>
<Field<string>
name="name"
validate={validateRequiredString}
render={({ input, meta }) => (
<div>
<input {...input} />
<ErrorMsg hidden={meta.touched} msgs={meta.error} />
</div>
)}
/>
<Field<string>
name="email"
validate={validateEmail}
render={({ input, meta }) => (
<div>
<input {...input} />
<ErrorMsg hidden={meta.touched} msgs={meta.error} />
</div>
)}
/>
<button disabled={!valid}>送信</button>
</form>
)}
/>
);
};
export default ReactFinalFormPage;
See also:
Final Form と、そのReactバインディングである React Final Form のドキュメントです
この動画でのライブコーディングも、大いに参考になると思います。