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
- Create a form
- Update the values of the form asynchronously
- 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();
}, []);
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.
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
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 /> |
---|---|
<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
Nothing important happens. We can ignore it.
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.