Skip to content

Commit

Permalink
Fix issue with inline isEqual causing an infinite rerender loop
Browse files Browse the repository at this point in the history
  • Loading branch information
Andarist committed Sep 12, 2019
1 parent 3add0f3 commit 5056cf9
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 22 deletions.
32 changes: 32 additions & 0 deletions src/Field.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Form onSubmit={onSubmitMock} initialValues={{ name: 'bob' }}>
{() => (
<form>
<Field
name="name"
isEqual={(a, b) =>
(a && a.toUpperCase()) === (b && b.toUpperCase())
}
>
{({ input, meta }) => (
<div>
<div data-testid="dirty">
{meta.dirty ? 'Dirty' : 'Pristine'}
</div>
<input {...input} data-testid="input" />
</div>
)}
</Field>
</form>
)}
</Form>
)
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()
Expand Down
50 changes: 28 additions & 22 deletions src/useField.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<FormValues: FormValuesShape>(
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<FormValues> = useForm<FormValues>('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
})

Expand Down Expand Up @@ -106,8 +113,7 @@ function useField<FormValues: FormValuesShape>(
// 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
Expand Down

0 comments on commit 5056cf9

Please # to comment.