React Final Form #03 かんたんに２回クリックの防止とサーバー側バリデーションを実装する

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

続き物です

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

# 1. 送信中にボタンを `disabled` にする 

送信ボタンを押した後、リクエストに対するレスポンスを待っている間にもう一度ボタンが押されてしまうと面倒なことになるので、**レスポンスを待っている間は、ボタンを押せないようにする**必要がありますよね。

[FormRenderProps](https://final-form.org/docs/final-form/types/FormState) の `submitting`プロパティは、`handleSubmit`で渡した`async`関数が終了するまでの間、`true`になります。これを使って、送信処理中にはボタンを`disabled`にし、ボタンの文言も **"送信中..."** に変更します。

`setTimeout`を追加しているのは、alertのポップアップが表示されて、"OK"ボタンを押すまでの間、`handleSubmit`が終了しないのを防ぐためです。

```diff_tsx:pages/react-final-forms.tsx
  const ReactFinalFormPage: NextPage = () => {
    type HandleSubmit = FormProps<ValuesType, ValuesType>["onSubmit"];
    const handleSubmit: HandleSubmit = async (s) => {
      await waitMs(1000);
+     setTimeout(() => {
        window.alert(`JSON: ${JSON.stringify(s)}`);
+     }, 0)
    };
    return (
      <Form
        onSubmit={handleSubmit}
        initialValues={initialValues}
-       render={({ handleSubmit, valid}) => (
+       render={({ handleSubmit, valid, submitting}) => (
          <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>
+            <button disabled={!valid || submitting}>{submitting ? "送信中..." : "送信"}</button>
          </form>
        )}
      />
    );
  };
  export default ReactFinalFormPage;

```

# 2. サーバー側バリデーションの追加

会員登録やログインの入力フォームでは、サーバーサイドでバリデーションが行われ、その結果を表示することがあります。

https://final-form.org/docs/react-final-form/examples/submission-errors

この例にあるように、 React Final Form には、サーバーサイドでのバリデーションに対応する機能も含まれています。


## `handleSubmit`関数の書き換え

```diff_tsx:handlesubmit
  const handleSubmit: HandleSubmit = async (s) => {
    await waitMs(1000);
+   if(s.email === "taro@example.com") {
+     return {
+       email: ["このeメールアドレスはすでに使われています。"]
+     }
+   }
    setTimeout(() => {
      window.alert(`JSON: ${JSON.stringify(s)}`);
    }, 0)
  };
```

`handleSubmit`関数において、 `{ フィールド名: エラー内容(any型) } ` の形式のオブジェクト (ここでは、仮に **Form Error Object** と呼びます) を返すことで、サーバーから返ってきた(バリデーション結果や、パスワード不一致等の)エラーの内容をフォームに渡すことができます。(エラーは throw してはいけません。 throw するのは、通信に関するエラー等です。)

今回はサーバーを作っていないため、このようなダミー実装になります。

## サーバーサイドバリデーションの結果の取り扱い

Form Render Props には、`submitFailed`, `dirtySinceLastSubmit` というプロパティがあるので、これを使ってフォーム全体のエラー状態を監視して、全体的なエラー表示とボタンの活・非活のをコントロールしてみましょう。

```diff_tsx:Formの部分
    <Form
      onSubmit={handleSubmit}
      initialValues={initialValues}
      render={({
        handleSubmit,
        valid,
        submitting,
+       submitFailed,
+       dirtySinceLastSubmit,
      }) => (
        <form onSubmit={handleSubmit}>
        {/* ... */}
+         <div>{submitFailed ? "登録に失敗しました。" : ""}</div>
-         <button disabled={!valid || submitting}>{submitting ? "送信中..." : "送信"}</button>
+         <button disabled={!(valid || dirtySinceLastSubmit) || submitting}>
+           {submitting ? "送信中..." : "送信"}
+         </button>
+       </form>
      )
```

### `submitFailed` プロパティ

`submitFailed` プロパティは、`handleSubmit`からエラーが返ってくると、`true` になります。(この時、同時に`valid` プロパティが `false` になります。) このプロパティが再び `false` になるのは、もう一度 submit した時です。

`submitFailed` が `true` である時には、「登録に失敗しました」のメッセージを表示するようにしています。


### `dirtySinceLastSubmit`プロパティ
`dirtySinceLastSubmit` プロパティは、エラーが返ってきたあと、フォームの状態に一項目でも差分があると `true` になります。

なので、そのような状態になるとボタンが再びクリック可能になるようにしています。

https://final-form.org/docs/final-form/types/FormState

# 3. サーバーサイドバリデーションの結果をフィールドに反映する

```diff_tsx:emailフィールドの状態
<Field<string>
    name="email"
    validate={validateEmail}
    render={({ input, meta }) => (
      <div>
        <input {...input} />
        <ErrorMsg hidden={meta.touched} msgs={meta.error} />
+       {!meta.dirtySinceLastSubmit &&
+         meta.submitError?.map((s: string) => (
+           <span key={s}>{s}</span>
+         ))}
      </div>
    )}
  />
```

`meta.error` には、#01で実装した、クライアントサイドバリエーションの結果が入っています。

`meta.submitError` には、それと同時に、そのフィールドに対するサーバーサイドバリデーションの結果として、 **Form Error Object** の、フィールド名に対応するプロパティ値が入ってきます。

```tsx
{ email: ["このeメールアドレスはすでに使われています。"] }
```

というオブジェクトを `handleSubmit` で返したときには、`["このeメールアドレスはすでに使われています。"]` という値が入ってきます。

`!meta.dirtySinceLastSubmit &&` という部分は、前述の `FormState.dirtySinceLastSubmit`と同様に、そのフィールドの内容が、前回の submit 時点から差分が出たときに、エラー表示を抑制するために書いています。

(本当は `ErrorMsg` コンポーネントを使いたいところなのですが、なぜかうまく動かないので長い記述になっています。)

https://final-form.org/docs/react-final-form/types/FieldRenderProps


<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);
    if (s.email === "taro@example.com") {
      return {
        email: ["このeメールアドレスはすでに使われています。"],
      };
    }
    setTimeout(() => {
      window.alert(`JSON: ${JSON.stringify(s)}`);
    }, 0);
  };
  return (
    <Form
      onSubmit={handleSubmit}
      initialValues={initialValues}
      render={({
        handleSubmit,
        valid,
        submitting,
        submitFailed,
        dirtySinceLastSubmit,
      }) => (
        <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} />
                {!meta.dirtySinceLastSubmit &&
                  meta.submitError?.map((s: string) => (
                    <span key={s}>{s}</span>
                  ))}
              </div>
            )}
          />
          <div>{submitFailed ? "登録に失敗しました。" : ""}</div>
          <button disabled={!(valid || dirtySinceLastSubmit) || submitting}>
            {submitting ? "送信中..." : "送信"}
          </button>
        </form>
      )}
    />
  );
};
export default ReactFinalFormPage;
```
</div></ditails>

# （番外編） フォーム全体のエラーを表す特殊なキー

```ts
import { FORM_ERROR } from 'final-form'
```

の形で import できる、[FORM_ERROR](https://final-form.org/docs/final-form/types/FormState) 文字列をキーにすることで、**Form Error Object** に、フォーム全体についてのエラーを含めることができます。

```ts:こんな感じ
{ [FORM_ERROR]: ["このフォームは今のままではいけないと思っています。"] }
```

ログインフォームなどでは、セキュリティ上の理由でログイン失敗の詳細を表示したくないことがあると思いますが、そのような時に役立つ機能ですね。

# See also:

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

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

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

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

今回の内容と関係のある、サーバーサイドバリデーションの結果の取り扱いのデモです。

https://final-form.org/docs/react-final-form/examples/submission-errors