From 5056cf926129535ba6597c869bd95ee6d472153f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sun, 25 Aug 2019 11:23:39 +0200 Subject: [PATCH] Fix issue with inline isEqual causing an infinite rerender loop --- src/Field.test.js | 32 ++++++++++++++++++++++++++++++ src/useField.js | 50 ++++++++++++++++++++++++++--------------------- 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/src/Field.test.js b/src/Field.test.js index 010ac25f..1d8a4d04 100644 --- a/src/Field.test.js +++ b/src/Field.test.js @@ -895,6 +895,38 @@ describe('Field', () => { expect(getByTestId('dirty')).toHaveTextContent('Pristine') }) + it('should be able to use inline isEqual to calculate dirty/pristine without falling into infinite rerender loop', () => { + const { getByTestId } = render( +
+ {() => ( + + + (a && a.toUpperCase()) === (b && b.toUpperCase()) + } + > + {({ input, meta }) => ( +
+
+ {meta.dirty ? 'Dirty' : 'Pristine'} +
+ +
+ )} +
+
+ )} + + ) + expect(getByTestId('input').value).toBe('bob') + expect(getByTestId('dirty')).toHaveTextContent('Pristine') + fireEvent.change(getByTestId('input'), { target: { value: 'bobby' } }) + expect(getByTestId('dirty')).toHaveTextContent('Dirty') + fireEvent.change(getByTestId('input'), { target: { value: 'BOB' } }) + expect(getByTestId('dirty')).toHaveTextContent('Pristine') + }) + it('should only call each field-level validation once upon initial mount', () => { const fooValidate = jest.fn() const barValidate = jest.fn() diff --git a/src/useField.js b/src/useField.js index 3cae080f..0892beea 100644 --- a/src/useField.js +++ b/src/useField.js @@ -24,49 +24,56 @@ const defaultFormat = (value: ?any, name: string) => const defaultParse = (value: ?any, name: string) => value === '' ? undefined : value +const defaultIsEqual = (a: any, b: any): boolean => a === b + function useField( name: string, - { + config: UseFieldConfig = {} +): FieldRenderProps { + const { afterSubmit, allowNull, - beforeSubmit, component, defaultValue, format = defaultFormat, formatOnBlur, initialValue, - isEqual, multiple, parse = defaultParse, subscription = all, type, - validate, validateFields, value: _value - }: UseFieldConfig = {} -): FieldRenderProps { + } = config const form: FormApi = useForm('useField') - const validateRef = useLatest(validate) - - const beforeSubmitRef = useLatest(() => { - if (formatOnBlur) { - const formatted = format(state.value, state.name) - if (formatted !== state.value) { - state.change(formatted) - } - } - return beforeSubmit && beforeSubmit() - }) + const configRef = useLatest(config) const register = (callback: FieldState => void) => form.registerField(name, callback, subscription, { afterSubmit, - beforeSubmit: () => beforeSubmitRef.current(), + beforeSubmit: () => { + const { + beforeSubmit, + formatOnBlur, + format = defaultFormat + } = configRef.current + + if (formatOnBlur) { + const { value } = ((form.getFieldState(state.name): any): FieldState) + const formatted = format(value, state.name) + + if (formatted !== value) { + state.change(formatted) + } + } + + return beforeSubmit && beforeSubmit() + }, defaultValue, - getValidator: () => validateRef.current, + getValidator: () => configRef.current.validate, initialValue, - isEqual, + isEqual: (a, b) => (configRef.current.isEqual || defaultIsEqual)(a, b), validateFields }) @@ -106,8 +113,7 @@ function useField( // If we want to allow inline fat-arrow field-level validation functions, we // cannot reregister field every time validate function !==. // validate, - initialValue, - isEqual + initialValue // The validateFields array is often passed as validateFields={[]}, creating // a !== new array every time. If it needs to be changed, a rerender/reregister // can be forced by changing the key prop