【React Final Form】formatOnBlurを使っているフォームを初期化するとフィールドの状態がおかしくなる(初期化されない etc..)

![format-complete.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/716b7674-5158-299b-628c-99a6ed807c03.gif)

# tl;dr


- React Final Form には、format機能があります。
  - フィールドの表示内容に対して、*全角英数→半角英数* のような変換処理が行えます。
- :warning: しかし、format機能を使うと、変換処理が走るたびにIMEが閉じてしまうので、IMEが使い物になりません。
- そこで、`formatOnBlur` prop を指定しましょう。
  - blur (つまりフォーカスが外れた)の瞬間にしか format が走らないようになり、
  - 変換処理のたびにIMEが閉じてしまうのを抑制できます。
- :warning: しかし、そうすると、フォームをリセットした時にフィールドの状態がおかしくなります。
- → **結論：`value={input.value || ""}` で 強制的に空文字列で上書きしてしまいましょう。**

# 環境・コード

以下のライブラリを使用しています。

| ライブラリ | ver |
|---|---|
| Next.js |10.1.3|
|React.js| 16.13.0|
|MUI|5.3.0|
|React Final Form|6.5.3|
|Final Form|4.20.2|
|TypeScript|4.0|

:::note
この記事では、簡単のため、Next.js, Type&shy;Script, MUI を用いています
:::

ソースコード全文を先に置いておきます。
(`Container` , `Stack` はレイアウト用のコンポーネントなので気にしないで下さい。)

```tsx:react-final-forms/format.tsx
import { NextPage } from "next";
import { Button, Container, Stack, TextField } from "@mui/material";
import { Field, Form, FormSpy } from "react-final-form";

// 住所に含まれる全角数字と長音記号を、半角数字・半角マイナス記号に変換
const normalizeAddress = (s: string | undefined): string => {
  // NOTE: (1) undefined -> "" (空文字列) のデフォルトの変換を再現
  if (!s) return "";
  // https://www.yoheim.net/blog.php?q=20191101
  return s
    .replace(/[−]/g, "-")
    .replace(/[０-９]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 0xfee0));
};

const ReactFinalFormFormatPage: NextPage = () => {
  return (
    <Container>
      <Form onSubmit={() => {}}>
        {({ handleSubmit, form }) => (
          <form onSubmit={handleSubmit}>
            <Stack py={4} spacing={2}>
              {/* NOTE: (2) IMEとの兼ね合いでformatOnBlurを使ってる */}
              <Field name="address" format={normalizeAddress} formatOnBlur>
                {({ input }) => (
                  <TextField
                    {...input}
                    // NOTE: (3) formatOnBlurを使っているので、実はここでも再び undefined を弾かないといけない
                    value={input.value || ""}
                    label="address"
                  />
                )}
              </Field>

              <Stack direction="row">
                <Button variant="contained" onClick={() => form.restart()}>
                  <code>form.restart()</code>
                </Button>
              </Stack>

              {/* フォームの値をJSON文字列の形で表示 */}
              <FormSpy>
                {({ values }) => (
                  <pre>
                    <code>{JSON.stringify(values, null, 2)}</code>
                  </pre>
                )}
              </FormSpy>
            </Stack>
          </form>
        )}
      </Form>
    </Container>
  );
};

export default ReactFinalFormFormatPage;

```



# React Final Form での空欄の扱い
まず、React Final Form が空のフィールドを保持しているとき、

- Form は内部的に `undefined` として格納し、
- Field コンポーネントがこれを空文字列 `""` に変換したうえでinput要素 (ここでは`TextField`を使用)に渡します。


React Final Form は **Controlled** Component として各要素を管理しますが、value が `undefined` になってしまうと、 **Uncontrolled** Component になってしまい、React Final Form による value の制御から離れてしまうため、と考えられます。 

# `format` prop 
`format` prop には、`(Form内での値) => (Fieldに渡すvalue)` の変換関数を渡すことが出来ますが、`(s: string | undefind) => string` の関数を渡すことで、文字列フォーマットのみが行われます。(数値への変換などは行わない)

ただし、`format` を指定したときには、デフォルトで行われていた `undefined -> ""` の変換が自動的に行われなくなる [^1] ため、変換関数の中で明示的に `if (!s) return "";`と書いています。**\[NOTE (1)\]**

```tsx:変換する関数の例
// 住所に含まれる全角数字と長音記号を、半角数字・半角マイナス記号に変換
const normalizeAddress = (s: string | undefined): string => {
  // NOTE: (1) undefined -> "" (空文字列) のデフォルトの変換を再現
  if (!s) return "";
  // https://www.yoheim.net/blog.php?q=20191101
  return s
    .replace(/[−]/g, "-")
    .replace(/[０-９]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 0xfee0));
};
```

```tsx:こんな感じで渡す
<Field 
  format={normalizeAddress}
>...
</Field>
```

# `formatOnBlur` prop

しかし、そのままでは問題があります。

IMEで入力している場合には、文字列の変換処理が走るたびにIMEが中断されてしまうのです。
(関数の入力と出力が同じであれば中断されませんが、`谷町「４(全角)」 → 谷町「4(半角)」` のように、差分が生じた瞬間に中断されます)

そこで、`formatOnBlur` propに `true` を渡すことで、上記の変換を行うタイミングを「フィールドからフォーカスが外れた瞬間」のみに限定することが出来ます。

IMEを邪魔しないためには、このpropを設定する必要があります。**\[NOTE (2)\]**

| `formatOnBlur` がある時 | `formatOnBlur` がない時 |
|---|---|
| ![format-success.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/286a9023-13a7-6943-2e88-a9d2a900f8c5.gif)| ![format-fail.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/2e65407e-f955-22e6-ae80-b9eecf705d9f.gif)|

```diff_tsx:こんな感じで渡す
  <Field 
    format={normalizeAddress}
+   formatOnBlur
  >...
  </Field>
```

# `formatOnBlur` による問題点
この「変換を行うタイミングを限定する」動作が裏目に出てしまう瞬間があります。 

それは**フォームがリセットされた瞬間**です。

フォームがリセットされて、該当するフィールドが `undefined` になった瞬間、formatの変換処理が実行されないので、Controlled Component にその `undefined` がそのまま渡されてしまいます。

**ここに意図せぬ Uncontrolled Component が誕生しました。**
内部的には `undefined` にリセットされているのに、`<TextField>`ではリセットする前の文字列が表示されたままになってしまいます。

![form-reset.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/b2d4fdca-25ff-8239-5cce-5811ecc9e041.gif)

そのためには、`format` の機能と別の方向で `undefined -> ""` の変換をしなければなりません。
そのために、`<TextField>` に渡す `value` を上書きして、明示的に `""` でフォールバックした訳です。[^2] **\[NOTE (3)\]**

```diff_tsx
  <Field name="address" format={normalizeAddress} formatOnBlur>
    {({ input }) => (
      <TextField
         {...input}
+        value={input.value || ""}
         label="address"
       />
     )}
  </Field>
```

![format-complete.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/716b7674-5158-299b-628c-99a6ed807c03.gif)


# 参考記事

https://www.yoheim.net/blog.php?q=20191101

https://final-form.org/docs/react-final-form/types/FieldProps#format

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


[^1]: ちょっと違うけど、[FieldProps#format のドキュメント](https://final-form.org/docs/react-final-form/types/FieldProps#format)より引用(太字強調は筆者による) ―― Note: If you would like to disable the default behavior of converting undefined to '', you can pass an identity function, v => v, to format. If you do this, **making sure your inputs are "controlled" is up to you.**
[^2]: `initialValues`propあるいは`reset(initialValues)`, `restart(initialValues)`などの引数に明示的に`""`を渡す方法もありますが、**フォームが管理する値(ビジネスロジックに直結)**と**フィールドの状態管理**を分離できたほうが気が楽だと思ったので、それらの方法は取りませんでした。