LoginSignup
4
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-01-18

format-complete.mov.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

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

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

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(/[0-9]/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)]

変換する関数の例
// 住所に含まれる全角数字と長音記号を、半角数字・半角マイナス記号に変換
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(/[0-9]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 0xfee0));
};
こんな感じで渡す
<Field 
  format={normalizeAddress}
>...
</Field>

formatOnBlur prop

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

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

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

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

formatOnBlur がある時 formatOnBlur がない時
format-success.mov.gif format-fail.mov.gif
こんな感じで渡す
  <Field 
    format={normalizeAddress}
+   formatOnBlur
  >...
  </Field>

formatOnBlur による問題点

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

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

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

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

form-reset.mov.gif

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

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

format-complete.mov.gif

参考記事


  1. ちょっと違うけど、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. initialValuespropあるいはreset(initialValues), restart(initialValues)などの引数に明示的に""を渡す方法もありますが、フォームが管理する値(ビジネスロジックに直結)フィールドの状態管理を分離できたほうが気が楽だと思ったので、それらの方法は取りませんでした。 

4
1
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
4
1