Redux Form の Field-Level Validation で無限ループ

2020/11/14に公開

もう随分と前から報告されている問題[1] なので今更だとは思うが、半年に一回は嵌っている落とし穴なので、ここに書き残しておきたい。

Redux Form では、以下のように、Field コンポーネントの validate 属性に関数を指定することで、フィールド単位で Validation を指定できる。これを Field-Level Validation という。validate 属性には配列を指定できるので、複数の関数を組み合わせることが可能だ。

const FieldLevelValidationForm = props => {
  const { handleSubmit, pristine, reset, submitting } = props;
  return (
    <form onSubmit={handleSubmit}>
      <Field
        name="username"
        type="text"
        validate={[required, maxLength15]}
      />
    </form>
  );
}

validate 属性に指定している関数のうち、たとえば、maxLength15 は以下のように実装されている。

const maxLength = max => value =>
  value && value.length > max ? `Must be ${max} characters or less` : undefined;
const maxLength15 = maxLength(15);

ところで、maxLength 関数は他のフォームでも利用できそうなので、別のモジュールに移動して再利用したくなるだろう。

import { maxLength } from 'util/validator/LengthValidator';

const maxLength15 = maxLength(15);

そして、ここまで来ると、maxLength15 変数が冗長に思えてくる。簡潔にこう書きたい誘惑に駆られる。

import { maxLength } from 'util/validator/LengthValidator';
...
      <Field
        name="username"
        type="text"
        validate={[required, maxLength(15)]}
      />

もちろん、Render のたびに関数が再作成されるコストはあるものの、たいていの場合、大きな問題にはならない[2]

だが、気が効いているように思えるこのちょっとした変更は、無情にもあなたのフォームを壊してしまう。

Violation: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
    at invariant (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:55:15)
    at scheduleWork (webpack:///./node_modules/react-dom/cjs/react-dom.development.js?:19916:5)
...

ログを確認してみると、@@redux-form/UNREGISTER_FIELD@@redux-form/REGISTER_FIELD のふたつのアクションがひたすら繰り返されている。無限ループだ。

実は、この問題については、公式の API リファレンスにも注意書きがある

Note: if the validate prop changes the field will be re-registered.

というわけで、validate 属性に渡す関数はコンポーネントの外側で定義したものを使うのが一番いいだろう。

validate 属性に渡す関数で props を使いたい

では、validate 属性に渡す関数で、コンポーネントに渡された props を参照したい場合はどうすればいいだろうか。これは簡単で、validate 属性に渡す関数にはフィールドの値 value の他にも以下の引数が渡される。

  • value - フィールドの値
  • allValues - フォームのすべてのフィールドの値
  • props - props
  • name - フィールド名

これで、props を参照する関数も実装できる。

const validateUniqueBook = (value, _values, props) => {
  const { books } = props;
  return books.every(b => b.isbn !== value) ? undefined : "You already have this book.";
});
脚注
  1. Field level validation & warning · Issue #4017 · redux-form/redux-form ↩︎

  2. Passing Functions to Components – React ↩︎

Discussion