form icon indicating copy to clipboard operation
form copied to clipboard

Programatic updates don't get shown on fields

Open nacho-vazquez opened this issue 1 year ago • 5 comments

Describe the bug

When updating the form value using either form.setFieldValue or

form.store.setState((state) => {
        return {
          ...state,
          values: {
            ...data,
          },
        };
      });

The state gets lost, and it is never shown in the field.

Your minimal, reproducible example

https://stackblitz.com/edit/stackblitz-starters-oj2quu?file=src%2FApp.tsx

Steps to reproduce

  1. Create a form
  2. Update the values of the form asynchronously
  3. See how the form input doesn't show the updated value.

Expected behavior

As a user, I expected to see the updated values populate the fields.

How often does this bug happen?

Every time

Screenshots or Videos

https://github.com/TanStack/form/assets/149403662/c09878d5-7597-4719-ab97-91bb0fa9e229

Platform

macOS - Chrome (Arc)

Tanstack Form adapter

react-form

TanStack Form version

0.13.3

TypeScript version

5.2.2

Additional context

No response

nacho-vazquez avatar Jan 10 '24 23:01 nacho-vazquez

UDPATE

  • At the beginning I thought it was cause by conditional rendering but I remove it and it continue to happen
  • Sometimes, the value flashes before getting cleared.

nacho-vazquez avatar Jan 11 '24 00:01 nacho-vazquez

Crazy enough, the problem seems to happen when the set state function setxData(data); is used

useEffect(() => {
    async function fetchData() {
      const p1 = new Promise<{ firstName: string }>((res) =>
        setTimeout(() => res({ firstName: 'Dave' }), 1000)
      );

      const data = await p1;

      setxData(data);

      form.store.setState((state) => {
        return {
          ...state,
          values: {
            ...data,
          },
        };
      });
    }

    fetchData();
  }, []);

nacho-vazquez avatar Jan 11 '24 00:01 nacho-vazquez

TL;DR

I looked into it and apparently when the state of the component in which the useForm() hook is updated, the component will be rerendered, which means that the useForm() hook runs once again and re-creates the form with the default values, ignoring the previous state of the form.

The code sandbox in the issue's description can be fixed, if the default values are set using xData:

  const [xData, setxData] = useState(null);

  const form = useForm<{ firstName: string }>({
    defaultValues: {
      firstName: xData.firstName,
    },
    onSubmit: async ({ value }) => {
      console.log(value);
    },
  });

Profiling the components (with React Dev Tools)

I compared how the same component (very similar to the one in the issue's description) changes when we update the component's state (<App />), vs when we don't.

1. Not modifying the state of <App /> during data fetching

1 commit.

image Field changes because one of its hooks. This must be because we changed its value when we called form.setFieldValue('firstName', data.firstName).

2. Modifying the state of <App /> during data fetching (second image)

3 commits.

Commit 1

image The Field rerenders, because one of its hooks has changed, I think this is when we update it through the api. I think this is the point when see the value we set in the Field.

Commit 2

<App /> <Field />
image image

<App /> rerenders, because one of its hooks has changed. I think this is the setXData hook.

This causes <Field/> to rerender too. This must be the point when our value disappears from the field and it starts with a blank state again.

Commit 3

image

Nothing important happens. We can ignore it.

fulopkovacs avatar Jan 13 '24 21:01 fulopkovacs

I could look into this issue even further (but unfortunately only after a few days), but I'm not that familiar with the internals of the form.

@crutchcorn can you give any pointers? My intuition is that form.Provider creates a new React context, in which case we might have a problem because I think that context is recreated when the component that contains form.Provider is rerendered. This kind of situation would be solvable by storing the state of the form outside of the component that contains it. We would have to use that data to set the initial values of the form when its parent component gets rerendered.

The first idea that comes to my mind for implementing this is a global form context that is at the root of the whole React app, but sounds like a pretty big change.

fulopkovacs avatar Jan 13 '24 21:01 fulopkovacs

@fulopkovacs, thanks for the research. This is great!

nacho-vazquez avatar Jan 13 '24 21:01 nacho-vazquez

Sorry for the long wait @nacho-vazquez!

There's been a few shifts since you last opened this issue, and there was a bug that was fixed, but your code sample still doesn't work out of the box.

That said, I'm unfortunately going to mark this as "working as intended" albiet confusingly. Here's why:

See, you can also change:

form.setFieldValue('firstName', 'Davex');

To:

form.setFieldValue('firstName', 'Davex', { touch: true });

To solve the issue.

By default, touch is false, which allows you to set the fieldValue programmatically internally without marking it as a user input

But when the re-render occurs with a new defaultValue, it throws away the previous field value since it's not a user input.

It's a bit confusing, but I generally consider the setX APIs in TanStack Form as internals.

Instead, we're investigating a story to support async default values easier.

Closing this for now, as a result of above, but don't be a stranger!

crutchcorn avatar Mar 18 '24 08:03 crutchcorn

It makes sense. I can make this work; thanks for looking into it.

nacho-vazquez avatar Mar 19 '24 14:03 nacho-vazquez

I'm running into a similar issue where I use field.setValue to programmatically update my form state but the UI doesn't reflect it. I'm new to this project so I'm probably missing something obvious!

Also found this https://github.com/TanStack/form/discussions/728

In my case I'm trying to update an item in an array programmatically.

devth avatar Aug 21 '24 14:08 devth

@devth you may be looking for Reactivity to explicitly subscribe to value changes.

Balastrong avatar Aug 21 '24 18:08 Balastrong

Thanks! I did indeed get it working with Subscribe. I think my biggest surprise / misunderstanding was having access to person in a loop like this, but not having that value update automatically like when using React's useState:

        <form.Field name="people" mode="array">
          {(field) => {
            return (
              <div>
                {field.state.value.map((person, i) => {
                  return (
                    <form.Field key={i} name={`people[${i}].name`}>
                      {(subField) => {
                        return (
                          <div>
                            <label>
                              <div>Name for person is {person.name} –– THIS won't update automatically!!</div>
                              <input
                                value={subField.state.value}
                                onChange={(e) =>
                                  subField.handleChange(e.target.value)
                                }
                              />
                            </label>
                          </div>
                        )
                      }}
                    </form.Field>
                  )
                })}
                <button
                  onClick={() => field.pushValue({ name: '', age: 0 })}
                  type="button"
                >
                  Add person
                </button>
              </div>
            )
          }}
        </form.Field>

My update was working correctly all along - I just didn't realize the value wouldn't reflect it automatically.

devth avatar Aug 21 '24 19:08 devth