diff --git a/src/Field.js b/src/Field.js
index baab6f01..4e7706e8 100644
--- a/src/Field.js
+++ b/src/Field.js
@@ -16,6 +16,7 @@ const Field = ({
formatOnBlur,
initialValue,
isEqual,
+ keepFieldStateOnUnmount,
multiple,
name,
parse,
@@ -38,6 +39,7 @@ const Field = ({
formatOnBlur,
initialValue,
isEqual,
+ keepFieldStateOnUnmount,
multiple,
parse,
subscription,
diff --git a/src/ReactFinalForm.test.js b/src/ReactFinalForm.test.js
index 5e38ac37..0de19009 100644
--- a/src/ReactFinalForm.test.js
+++ b/src/ReactFinalForm.test.js
@@ -912,7 +912,8 @@ describe('ReactFinalForm', () => {
it('should allow an alternative form api to be passed in', () => {
const onSubmit = jest.fn()
const form = createForm({ onSubmit: onSubmit })
- const formMock = jest.spyOn(form, 'registerField')
+ const registerFieldSpy = jest.spyOn(form, 'registerField')
+ const subscribeToFieldStateSpy = jest.spyOn(form, 'subscribeToFieldState')
render(
)
- expect(formMock).toHaveBeenCalled()
- // called once on first render to get initial state, and then again to subscribe
- expect(formMock).toHaveBeenCalledTimes(2)
- expect(formMock.mock.calls[0][0]).toBe('name')
- expect(formMock.mock.calls[0][2].active).toBe(true) // default subscription
- expect(formMock.mock.calls[1][0]).toBe('name')
- expect(formMock.mock.calls[1][2].active).toBe(true) // default subscription
+ // called once on first render to register only once
+ expect(registerFieldSpy).toHaveBeenCalledTimes(1)
+ expect(registerFieldSpy.mock.calls[0][0]).toBe('name')
+ expect(registerFieldSpy.mock.calls[0][1]).toBe(undefined) // no initial callback
+ expect(registerFieldSpy.mock.calls[0][2]).toBe(undefined) // no subscription
+
+ // subscribe to field state once
+ expect(subscribeToFieldStateSpy).toHaveBeenCalledTimes(1)
+ expect(subscribeToFieldStateSpy.mock.calls[0][0]).toBe('name')
+ expect(subscribeToFieldStateSpy.mock.calls[0][2].active).toBe(true) // default subscription
+ })
+
+ it('should keep field states when field is hidden with keepFieldStateOnUnmount option', () => {
+ const { getByTestId, getByText } = render(
+
+ {hidden => (
+
+ )}
+
+ )}
+
+ )
+
+ const nameField = getByTestId('name')
+ const errorElem = getByTestId('error')
+ expect(nameField).toHaveValue('erikras')
+ expect(errorElem).not.toHaveTextContent('SHOULD BE LOWERCASE')
+
+ fireEvent.change(nameField, { target: { value: 'ERIKRAS' } })
+
+ expect(nameField).toHaveValue('ERIKRAS')
+ expect(errorElem).toHaveTextContent('SHOULD BE LOWERCASE')
+
+ const toggleButton = getByText('Toggle')
+ // hide
+ fireEvent.click(toggleButton)
+ expect(nameField).not.toBeInTheDocument()
+
+ // show
+ fireEvent.click(toggleButton)
+ expect(nameField).toHaveValue('ERIKRAS')
+ expect(errorElem).toHaveTextContent('SHOULD BE LOWERCASE')
+ })
+
+ it('should not re-register when hidden field becomes visible again with keepFieldStateOnUnmount option', () => {
+ const onSubmit = jest.fn()
+ const form = createForm({ onSubmit: onSubmit })
+ const registerFieldSpy = jest.spyOn(form, 'registerField')
+ const subscribeToFieldStateSpy = jest.spyOn(form, 'subscribeToFieldState')
+
+ const { getByTestId, getByText } = render(
+
+ {hidden => (
+
+ )}
+
+ )}
+
+ )
+
+ const toggleButton = getByText('Toggle')
+ expect(registerFieldSpy).toHaveBeenCalledTimes(1)
+ expect(subscribeToFieldStateSpy).toHaveBeenCalledTimes(1)
+
+ // hide
+ fireEvent.click(toggleButton)
+ expect(registerFieldSpy).toHaveBeenCalledTimes(1)
+ expect(subscribeToFieldStateSpy).toHaveBeenCalledTimes(1)
+
+ // show
+ fireEvent.click(toggleButton)
+ expect(registerFieldSpy).toHaveBeenCalledTimes(1)
+ expect(subscribeToFieldStateSpy).toHaveBeenCalledTimes(2)
+ })
+
+ it('should re-register with the name prop change', () => {
+ const onSubmit = jest.fn()
+ const form = createForm({ onSubmit: onSubmit })
+ const registerFieldSpy = jest.spyOn(form, 'registerField')
+ const subscribeToFieldStateSpy = jest.spyOn(form, 'subscribeToFieldState')
+
+ const { getByTestId, getByText } = render(
+
+ {isCat => (
+
+ )}
+
+ )}
+
+ )
+
+ const nameField = getByTestId('name')
+ const toggleButton = getByText('Toggle')
+ expect(registerFieldSpy).toHaveBeenCalledTimes(1)
+ expect(subscribeToFieldStateSpy).toHaveBeenCalledTimes(1)
+
+ // change to the input field shouldn't trigger re-register
+ fireEvent.change(nameField, { target: { value: 'Jon' } })
+ expect(registerFieldSpy).toHaveBeenCalledTimes(1)
+ expect(subscribeToFieldStateSpy).toHaveBeenCalledTimes(1)
+
+ fireEvent.click(toggleButton)
+ expect(registerFieldSpy).toHaveBeenCalledTimes(2)
+ expect(subscribeToFieldStateSpy).toHaveBeenCalledTimes(2)
+
+ fireEvent.click(toggleButton)
+ expect(registerFieldSpy).toHaveBeenCalledTimes(3)
+ expect(subscribeToFieldStateSpy).toHaveBeenCalledTimes(3)
+ })
+
+ it('should re-register with the name prop change with keepFieldStateOnUnmount', () => {
+ const onSubmit = jest.fn()
+ const form = createForm({ onSubmit: onSubmit })
+ const registerFieldSpy = jest.spyOn(form, 'registerField')
+ const subscribeToFieldStateSpy = jest.spyOn(form, 'subscribeToFieldState')
+
+ const { getByTestId, getByText } = render(
+
+ {isCat => (
+
+ )}
+
+ )}
+
+ )
+
+ const nameField = getByTestId('name')
+ const toggleButton = getByText('Toggle')
+ expect(registerFieldSpy).toHaveBeenCalledTimes(1)
+ expect(subscribeToFieldStateSpy).toHaveBeenCalledTimes(1)
+
+ // change to the input field shouldn't trigger re-register
+ fireEvent.change(nameField, { target: { value: 'Jon' } })
+ expect(registerFieldSpy).toHaveBeenCalledTimes(1)
+ expect(subscribeToFieldStateSpy).toHaveBeenCalledTimes(1)
+
+ fireEvent.click(toggleButton)
+ expect(registerFieldSpy).toHaveBeenCalledTimes(2)
+ expect(subscribeToFieldStateSpy).toHaveBeenCalledTimes(2)
+
+ // this should change the name back to cat (or dog, whatever toggle toggles the toggle)
+ // since the states weren't removed, registration should not happen again, but subscription to field will
+ fireEvent.click(toggleButton)
+ expect(registerFieldSpy).toHaveBeenCalledTimes(2)
+ expect(subscribeToFieldStateSpy).toHaveBeenCalledTimes(3)
})
it('should not destroy on unregister on initial unregister', () => {
diff --git a/src/types.js.flow b/src/types.js.flow
index 5784a865..a06d7143 100644
--- a/src/types.js.flow
+++ b/src/types.js.flow
@@ -94,6 +94,7 @@ export type UseFieldAutoConfig = {
formatOnBlur?: boolean,
initialValue?: any,
isEqual?: (a: any, b: any) => boolean,
+ keepFieldStateOnUnmount?: boolean,
multiple?: boolean,
parse?: (value: any, name: string) => any,
type?: string,
diff --git a/src/useField.js b/src/useField.js
index 48a52499..7034441c 100644
--- a/src/useField.js
+++ b/src/useField.js
@@ -39,6 +39,7 @@ function useField(
format = defaultFormat,
formatOnBlur,
initialValue,
+ keepFieldStateOnUnmount = false,
multiple,
parse = defaultParse,
subscription = all,
@@ -50,13 +51,13 @@ function useField(
const configRef = useLatest(config)
- const register = (callback: FieldState => void) =>
+ const register = () =>
// avoid using `state` const in any closures created inside `register`
// because they would refer `state` from current execution context
// whereas actual `state` would defined in the subsequent `useField` hook
// execution
// (that would be caused by `setState` call performed in `register` callback)
- form.registerField(name, callback, subscription, {
+ form.registerField(name, undefined, undefined, {
afterSubmit,
beforeSubmit: () => {
const {
@@ -84,49 +85,61 @@ function useField(
validateFields
})
- const firstRender = React.useRef(true)
+ const firstRenderRef = React.useRef(true)
+ const unregisterRef = React.useRef()
+
+ const registerAndGetInitialState: () => FieldState = () => {
+ let initState
+ if (!keepFieldStateOnUnmount) {
+ unregisterRef.current && unregisterRef.current()
+ unregisterRef.current = undefined
+ } else {
+ initState = form.getFieldState(name)
+ }
+
+ // if no initial state, register!
+ if (!initState) {
+ const unregisterFn = register()
+ // only set unregister function when keepFieldStateOnUnmount option is false
+ if (keepFieldStateOnUnmount === false) {
+ unregisterRef.current = unregisterFn
+ }
+ initState = form.getFieldState(name)
+ }
- // synchronously register and unregister to query field state for our subscription on first render
- const [state, setState] = React.useState((): FieldState => {
- let initialState: FieldState = {}
+ return initState || {}
+ }
- // temporarily disable destroyOnUnregister
- const destroyOnUnregister = form.destroyOnUnregister
- form.destroyOnUnregister = false
+ // register on first render
+ // this will initially check for existing field state from the form before trying to register
+ const [state, setState] = React.useState(
+ registerAndGetInitialState
+ )
- register(state => {
- initialState = state
- })()
+ React.useEffect(
+ () => {
+ // make sure this doesn't get triggered on first render
+ if (firstRenderRef.current === false) {
+ unregisterRef.current && unregisterRef.current()
+ setState(registerAndGetInitialState())
+ }
- // return destroyOnUnregister to its original value
- form.destroyOnUnregister = destroyOnUnregister
+ const unsubscribeFieldState = form.subscribeToFieldState(
+ name,
+ setState,
+ subscription
+ )
- return initialState
- })
+ firstRenderRef.current = false
- React.useEffect(
- () =>
- register(state => {
- if (firstRender.current) {
- firstRender.current = false
- } else {
- setState(state)
- }
- }),
+ return () => {
+ unsubscribeFieldState()
+ unregisterRef.current && unregisterRef.current()
+ unregisterRef.current = undefined
+ }
+ },
// eslint-disable-next-line react-hooks/exhaustive-deps
- [
- name,
- data,
- defaultValue,
- // If we want to allow inline fat-arrow field-level validation functions, we
- // cannot reregister field every time validate function !==.
- // validate,
- 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
- // validateFields
- ]
+ [defaultValue, initialValue, name]
)
const handlers = {