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

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

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

続き物です

- [#01『かんたんにフォームを構築する』](https://qiita.com/honey32/items/c919aacbbf404db1b8b8)
- **#02『Render Props を使って、かんたんにバリデーションを実装する』 ←今ここ**
- [#03『かんたんに２回クリックの防止とサーバー側バリデーションを実装する』](https://qiita.com/honey32/items/3ed19b950d7aeecbd4cf)

# 0. emailアドレス欄を追加

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

```diff_tsx: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 ライブラリを使う際ためには、おそらくこの書き方に変える必要があるでしょう。

```diff_tsx: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](https://final-form.org/docs/react-final-form/types/FieldRenderProps) 型になります。 関数の引数オブジェクトの `input` プロパティはオブジェクトになっています。基本的には、この内容を全て `<input/>` 要素に渡すことになっています。スプレッド構文を使ってまとめて渡しましょう。

# 2. バリデータの追加

それでは、お待ちかねのバリデーションの実装に移ります。
次の const 宣言をトップレベルに書きましょう。

```tsx: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[]`型の値に決めていますが、返り値の型は元々決まっていないので、自分で決めましょう。)

メールアドレスの正規表現は下のページを参考にしています。

https://www.javadrive.jp/regex-basic/sample/index13.html#section1

これを、Fieldのコンポーネントに実装していきましょう。

```tsx: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 &&` を付けていますが、これは、

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

ことを表しています。

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

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

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

```diff_tsx: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](https://final-form.org/docs/react-final-form/types/FormRenderProps) ) には、`valid` Prop があります。中のフィールドのバリデーションが全て成功している時にこの値が `true` になります。
これによって、バリデーションエラーが出ているときに、ボタンを押せなくすることができました。

# 4. エラーメッセージをコンポーネント化

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

```diff_tsx: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>
  )}
/>
```

```tsx:コンポーネントの定義
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. 名前にもバリデーションをつける

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

```tsx:pages/react-final-forms/index.tsx
const validateRequiredString: FieldValidator<string> = (
  value
): string[] | undefined => {
  if (!value || value.length === 0) return ["必須要素です"];
  return undefined;
};
```

```diff_tsx: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 を使えば、一貫した方法で柔軟にフィールドを制御したり、バリデーションを実装したりできることがわかったと思います。
送信ボタンの活/不活も、同様のインターフェースで制御できましたね。
次回以降はまだ未定です。

<details><summary>コード全容</summary><div>

```tsx: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;
```
</div></details>

# See also:

Final Form と、そのReactバインディングである React Final Form のドキュメントです

https://final-form.org/docs/react-final-form/getting-started

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

https://youtu.be/WoSzy-4mviQ