TanStack / form

🤖 Powerful and type-safe form state management for the web. TS/JS, React Form, Solid Form, Lit Form and Vue Form.

Home Page:https://tanstack.com/form

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Programatic updates don't get shown on fields

nacho-vazquez opened this issue · 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

Screen.Recording.2024-01-10.at.20.51.53.mov

Platform

macOS - Chrome (Arc)

Tanstack Form adapter

react-form

TanStack Form version

0.13.3

TypeScript version

5.2.2

Additional context

No response

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.

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();
  }, []);
commented

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.

commented

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, thanks for the research. This is great!

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!

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