6
2

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.

React Final Form #02 Render Props を使って、かんたんにクライアント側バリデーションを実装する

Last updated at Posted at 2021-06-04

この記事を読むと幸せになれそうな人

Controlled Component (React.useState や Redux を使う方法)で大きめのフォームを構築するとき、UIライブラリを使ったり、バリデーションを実装したりすると、ボイラープレートコードが多すぎて辟易してしまうと思います。
そんな人には、この React Final Form がオススメかもしれません。

続き物です

0. emailアドレス欄を追加

まずは、前回に続いて、emailアドレスの入力欄を追加します。(バリデーションの題材としてはこっちの方が手っ取り早いので)

pages/react-final-forms/index.tsx
  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 ライブラリを使う際ためには、おそらくこの書き方に変える必要があるでしょう。

pages/react-final-forms/index.tsx
- <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 宣言をトップレベルに書きましょう。

pages/react-final-forms/index.tsx
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のコンポーネントに実装していきましょう。

pages/react-final-forms/index.tsx
<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 && を付けていますが、これは、

  • errorundefined なら何も表示しない。
  • フィールドが、フォーカスを外されたことがない場合にも、何も表示しない。
    • meta.touched は、一度フォーカスされたあとフォーカスが外れたら true になるフラグ

ことを表しています。

Field Render Props のドキュメントを見ると、他にも様々なフラグ(meta.xxx)があります。それらを使うと、エラーメッセージの表示の切り替えのタイミングを様々に変えたり、初期値から変更されたフィールドだけを強調したりすることもできます。

3. バリデーションが成功していない場合にボタンをdisabledにする

ついでに、バリデーションが成功していない場合には、ボタンを disabled にしてしまいましょう。
こうしておかないと、入力値が間違っていても送信できてしまいますからね。

pages/react-final-forms/index.tsx
  <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. エラーメッセージをコンポーネント化

エラーメッセージの記述が長ったらしくなっていたので、コンポーネントに切り出して、記述を楽にします。

pages/react-final-forms/index.tsx
<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. 名前にもバリデーションをつける

名前にも、文字列が空のときにエラーを返すバリデータを追加します。
コンポーネント化したので、コードも短くてラクラクですね。

pages/react-final-forms/index.tsx
const validateRequiredString: FieldValidator<string> = (
  value
): string[] | undefined => {
  if (!value || value.length === 0) return ["必須要素です"];
  return undefined;
};
pages/react-final-forms/index.tsx
          <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 を使えば、一貫した方法で柔軟にフィールドを制御したり、バリデーションを実装したりできることがわかったと思います。
送信ボタンの活/不活も、同様のインターフェースで制御できましたね。
次回以降はまだ未定です。

コード全容
pages/react-final-forms/index.tsx
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 のドキュメントです

この動画でのライブコーディングも、大いに参考になると思います。

6
2
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
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?