react-final-form icon indicating copy to clipboard operation
react-final-form copied to clipboard

Bug: useField can not get newest value

Open xxleyi opened this issue 2 years ago • 7 comments

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

xxleyi avatar Apr 14 '22 01:04 xxleyi

Experiencing same issue. @xxleyi Did you manage to find any workaround?

Bump.

makap0120 avatar Apr 22 '22 08:04 makap0120

@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.

xxleyi avatar Apr 22 '22 09:04 xxleyi

@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.

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..

makap0120 avatar Apr 22 '22 11:04 makap0120

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:

  1. On the initial render, the state is set by registering the field, then immediately unregistering it, inside a useState(() => initializerCallback).
  2. Rendering continues down the tree.
  3. Once the effect phase is reached, the "permanent" call to registerField is made in a useEffect 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.)

wilysword avatar Sep 01 '22 20:09 wilysword

@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

xxleyi avatar Oct 08 '22 01:10 xxleyi

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 :/

iamdey avatar Nov 07 '22 11:11 iamdey

@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];
};

montes5 avatar Feb 07 '23 22:02 montes5