LoginSignup
2
0

Formik.setFieldValue と条件付き Schema を使う際の落とし穴

Posted at

概要

Formik の setFieldValue を 条件付き schema と合わせて使う際にちょっと困ったことがありました。その説明と対策について書きます。

問題

<Formik
  initialValues={{ a: "", b: "" }}
  validationSchema={yup.object({
    b: yup.string().when("a", {
      is: "a",
      then: (schema) => schema.oneOf(["a", "A"]),
    }),
  })}
  onSubmit={() => {}}
>
  {({ setFieldValue, errors }) => (
    <form>
      <button
        type="button"
        onClick={() => {
          setFieldValue("a", "a");
          setFieldValue("b", "error");
        }}
      >
        TEST
      </button>
      <p>
        <label>
          A
          <Field type="text" name="a" />
        </label>
      </p>
      <p>
        <label>
          B
          <Field type="text" name="b" />
        </label>
      </p>
      <pre>ERRORS: {JSON.stringify(errors, undefined, 2)}</pre>
    </form>
  )}
</Formik>

2つのフィールド a, b を持ち、フィールドa の値が "a" だった場合、フィールドb の値が "a" or "A" であることを制約条件としています。問題のコードはここ。

setFieldValue("a", "a");
setFieldValue("b", "error");

御覧の通り、ここで設定している値は前述の制約条件に違反しているため、本来であればエラーが表示されるはずです。しかしこれはエラーになりません。なぜでしょうか?

原因

setFieldValue を呼び出すと、Formik は Validation を行います。この時、その評価対象となる値は「現在値 + 設定された値」となります。上記コードでは setFieldValue が2回に分けて実行されているため、ここでは2つの Validation が発火されます。例えば、現在値が { a: "foo", b: "bar" } だった場合は次のとおり。

setFieldValue("a", "a");      // { a: "a", b: "bar" }
setFieldValue("b", "error");  // { a: "foo", b: "error" }

このため、最終的にセットされる値は { a: "a", b: "error" } であるにも関わらず、フォームはエラーとして評価されないという結果になります。

解決策

複数の値を同時にセットする場合は、setFieldValue ではなく、setValues を使いましょう。

setValues({ ...values, a: "a", b: "error" })

さらなる課題

setValues を使いましょうとは言ったものの、実はこれにもまだ欠点があります。この設定処理がAPIの処理結果を受けて行われる非同期処理だった場合どうなるでしょうか?

setTimeout(() => formik.setValues({ ...values, a: "a", b: "errors" }), 2000);

ここで、...values で現在値をマージしていることが問題になります。非同期処理実行中もフォームの編集が可能な場合、この values は古い値を参照することになります、つまり、この非同期処理が実行されるとき、ユーザーが入力した値が破棄されてしまう可能性があります。

この場合、useRef を使って最新の Formik instance を参照する必要があります。

  {(formik) => {
    formikRef.current = formik;
    return (
      <form>
        <button
          type="button"
          onClick={() => {
            setTimeout(() => {
              formikRef.current.setValues({
                ...(formikRef.current.values || {}),
                a: "a",
                b: "errors",
              });
            }, 2000);
          }}
        >
          TEST
        </button>      

まとめ

最後に挙げた非同期処理の問題は、よっぽど遅い非同期処理でなければ発生することは稀なのですが、Unit Test ではUI操作が高速に行われるため高い頻度で発生します。しかもエラーの原因がなかなか特定できずに苦労します、というかしました。逆に言えば、手動で操作していては気づきにくい問題も、テストを書くことで検出できるということ。

結論: テストを書こう。

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