react-final-form
react-final-form copied to clipboard
Bug: useField can not get newest value
When I change form value in useEffect without event trigger or network delay, useField can not get the newest value.
Demo link is here: https://codesandbox.io/s/react-final-form-usefield-can-not-get-newest-value-gc3do4?file=/index.js
Experiencing same issue. @xxleyi Did you manage to find any workaround?
Bump.
@makap0120 Just like in demo, we are using useFormState
to workaround this, but it is annoying, because we can not just use useFormState
everywhere, only after we were bitten by some bugs.
@makap0120 Just like in demo, we are using
useFormState
to workaround this, but it is annoying, because we can not just useuseFormState
everywhere, only after we were bitten by some bugs.
Thanks for quick reply :)
Oh i see, this doesn't seem as optimal solution in my case - to display field value i'm using renderProps.input.value
received from wrapper <Field subscribe={{value: true}} />
component and reusing corresponding field name prop. This being done for performance reasons, so would like to avoid hooking into form state for same reason.
I wonder why field subscribers miss this exact field change value change performed in useEffect during initial render..
This same bug applies to useFormState
; it specifically has to do with the firstRender
ref (https://github.com/final-form/react-final-form/blob/main/src/useField.js#L116). Here's what I think happens:
- On the initial render, the state is set by registering the field, then immediately unregistering it, inside a
useState(() => initializerCallback)
. - Rendering continues down the tree.
- Once the effect phase is reached, the "permanent" call to
registerField
is made in auseEffect
hook; however, a ref is used so that on the first call to the state callback, the state is not set.
It does this to avoid a double render, but it assumes that the state cannot change between the initial render and the effect phase, which as your use case illustrates, is incorrect. The effect phase runs depth-first, so any number of effect hooks can change the form state between the initializer and the permanent subscription. In your example specifically, the effect hook in the LastName
component runs before the one in FavoriteColor
, meaning the subscription in FavoriteColor
just misses the change.
(I was looking into it because I was using a FormSpy
subscribed to hasValidationErrors
, and it was returning false despite a number of field-level validators returning initial errors on required fields.)
@makap0120 Recently I am trying create a custom hook to reactive some (not all) values of final form by use of useSyncExternalStore
, here is the demo: https://github.com/xxleyi/learning_list/issues/336
Hi, I think I have experienced the same issue but in a different case: with conditional fields.
Even with Field
component, it won't retrieve the programmatically changed value on first render:
https://codesandbox.io/s/react-final-form-conditional-fields-forked-pllcsp?file=/src/index.js
I don't think I can use the useFormState
as workaround in that case :/
@xxleyi @makap0120 @wilysword @iamdey May be helpful for those who's still looking for a workaround
import { FormApi } from 'final-form';
import { useForm } from 'react-final-form';
import { useState, useEffect, useCallback } from 'react';
import _get from 'lodash/get';
type GetFormValue = (form: FormApi, name: string) => any;
const getFormValue: GetFormValue = (form, name) => _get(form.getState().values, name) ?? '';
type UseFormValue = <V, >(name: string) => [V, (v: V) => void];
export const useFormValue: UseFormValue = (name) => {
const form = useForm();
const [, updateComponent] = useState(0);
let value = getFormValue(form, name);
const setValue = useCallback((v: any) => form.change(name, v), [name]);
useEffect(() => {
const unsubscribe = form.subscribe(
({ values }) => {
const formValue = _get(values, name) ?? '';
if (formValue !== value) {
value = formValue;
updateComponent((v) => v + 1);
}
},
{ values: true },
);
return () => {
unsubscribe();
};
}, [name, value]);
return [value, setValue];
};