fix(react): form.reset not working inside of onSubmit
Relates to #1490, #1485 and potentially #1487.
This appears to stem from the react adapter as Angular, Vue and Core all seem to work as intended.
Reset will work correctly initially then on next pass wipe the previous state. Curiously this seems to only be the case in the onSubmit handler.
View your CI Pipeline Execution β for commit 9c3ccc862875c294df712e2e9244471d3af345b7
| Command | Status | Duration | Result |
|---|---|---|---|
nx affected --targets=test:sherif,test:knip,tes... |
β Succeeded | 1m 1s | View β |
nx run-many --target=build --exclude=examples/** |
β Succeeded | 6s | View β |
βοΈ Nx Cloud last updated this comment at 2025-07-07 13:13:08 UTC
More templates
- @tanstack/form-example-angular-array
- @tanstack/form-example-angular-large-form
- @tanstack/form-example-angular-simple
- @tanstack/form-example-lit-simple
- @tanstack/form-example-lit-ui-libraries
- @tanstack/form-example-react-array
- @tanstack/form-example-react-compiler
- @tanstack/field-errors-from-form-validators
- @tanstack/form-example-react-large-form
- @tanstack/form-example-react-next-server-actions
- @tanstack/form-example-react-query-integration
- @tanstack/form-example-remix
- @tanstack/form-example-react-simple
- @tanstack/form-example-react-standard-schema
- @tanstack/form-example-react-tanstack-start
- @tanstack/form-example-react-ui-libraries
- @tanstack/form-example-solid-array
- @tanstack/form-example-solid-large-form
- @tanstack/form-example-solid-simple
- @tanstack/form-example-svelte-array
- @tanstack/form-example-svelte-simple
- @tanstack/form-example-vue-array
- @tanstack/form-example-vue-simple
@tanstack/angular-form
npm i https://pkg.pr.new/@tanstack/angular-form@1494
@tanstack/form-core
npm i https://pkg.pr.new/@tanstack/form-core@1494
@tanstack/lit-form
npm i https://pkg.pr.new/@tanstack/lit-form@1494
@tanstack/react-form
npm i https://pkg.pr.new/@tanstack/react-form@1494
@tanstack/solid-form
npm i https://pkg.pr.new/@tanstack/solid-form@1494
@tanstack/svelte-form
npm i https://pkg.pr.new/@tanstack/svelte-form@1494
@tanstack/vue-form
npm i https://pkg.pr.new/@tanstack/vue-form@1494
commit: 9c3ccc8
I'm not sure if this is the proper fix, but this fixes the newly added test without introducing other test failures.
Test is failing given opts doesn't contain the latest defaultValues as the latest values were passed via reset and not via props, therefore calling update(opts) here is actually overriding the new default values set via reset().
diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx
index 729064ab..dd7a012c 100644
--- a/packages/react-form/src/useForm.tsx
+++ b/packages/react-form/src/useForm.tsx
@@ -210,13 +210,5 @@ export function useForm<
useStore(formApi.store, (state) => state.isSubmitting)
- /**
- * formApi.update should not have any side effects. Think of it like a `useRef`
- * that we need to keep updated every render with the most up-to-date information.
- */
- useIsomorphicLayoutEffect(() => {
- formApi.update(opts)
- })
-
return formApi
}
@bpinto thanks for taking a look into this, I was rather busy over the weekend, so it's super appreciated!
hmm thats, interesting... I'll shoot crutch corn a message since he authored the code, but you are correct it dose pass the tests, though to play devils advocate we are lacking framework specific tests so it might be breaking something. π
Once again thanks for the investigation!
[edit] So to me removing the update(ops) won't work as we need to update the opts passed to form if they change. It's really a question as to what is causing the re-render of the form.
these lines changing this.options appear to be the cause. Commenting them out makes the test pass.
Of course it breaks everything else, but perhaps this could help with resolving this.
Codecov Report
All modified and coverable lines are covered by tests :white_check_mark:
Project coverage is 89.55%. Comparing base (
824d723) to head (9c3ccc8).
Additional details and impacted files
@@ Coverage Diff @@
## main #1494 +/- ##
==========================================
- Coverage 89.55% 89.55% -0.01%
==========================================
Files 34 34
Lines 1494 1493 -1
Branches 370 370
==========================================
- Hits 1338 1337 -1
Misses 139 139
Partials 17 17
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
:rocket: New features to boost your workflow:
- :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
- :package: JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
Testing on stackblitz, this appears to work as expected in react now!
@harry-whorlow I tested out this PR, and I see a UI glitch whenever I reset the form with the new values from the API. The initial values are briefly displayed and then replaced with the new values.
@harry-whorlow I tested out this PR, and I see a UI glitch whenever I reset the form with the new values from the API. The initial values are briefly displayed and then replaced with the new values.
From discord correct? this was unrelated if I'm not mistaken. π€
So this is not an ideal solution, and we've talked about this internally, however we couldnβt come up with an alternative solution.
I'll leave this up till Friday, so if anyone has any suggestions or other concerns they can post them here and I'll investigate. Otherwise I'll merge π
I am not sure what type of API is meant to be implemented here so I wrote a failing tests below that may or may not be failing correctly.
On this test, one of the form options asyncDebounceMs is being updated which causes the if (!evaluate(opts, stableOptsRef.current) to evaluate true.
This is an alternative way of introducing the bug that this PR is trying to fix as the defaultValues of the useForm() overrides the latest default value specified via reset().
diff --git a/packages/react-form/tests/useForm.test.tsx b/packages/react-form/tests/useForm.test.tsx
index 7b012db7..d042c095 100644
--- a/packages/react-form/tests/useForm.test.tsx
+++ b/packages/react-form/tests/useForm.test.tsx
@@ -797,7 +797,10 @@ describe('useForm', () => {
it('form should reset default value when resetting in onSubmit', async () => {
function Comp() {
+ const [asyncDebounceMs, setAsyncDebounceMs] = useState(0)
+
const form = useForm({
+ asyncDebounceMs,
defaultValues: {
name: '',
},
@@ -835,6 +838,10 @@ describe('useForm', () => {
<button type="reset" data-testid="reset" onClick={() => form.reset()}>
Reset
</button>
+
+ <button type="button" data-testid="update-form-options" onClick={() => setAsyncDebounceMs(10)}>
+ Update form options
+ </button>
</form>
)
}
@@ -843,6 +850,7 @@ describe('useForm', () => {
const input = getByTestId('fieldinput')
const submit = getByTestId('submit')
const reset = getByTestId('reset')
+ const updateFormOptions = getByTestId('update-form-options')
await user.type(input, 'test')
await waitFor(() => expect(input).toHaveValue('test'))
@@ -853,6 +861,9 @@ describe('useForm', () => {
await user.type(input, 'another-test')
await user.click(submit)
await waitFor(() => expect(input).toHaveValue('another-test'))
+
+ await user.click(updateFormOptions)
+ await waitFor(() => expect(input).toHaveValue('another-test'))
})
it('form should update when props are changed', async () => {
A smaller patch simulating this issue:
diff --git a/packages/react-form/tests/useForm.test.tsx b/packages/react-form/tests/useForm.test.tsx
index 7b012db7..d129ad8e 100644
--- a/packages/react-form/tests/useForm.test.tsx
+++ b/packages/react-form/tests/useForm.test.tsx
@@ -797,7 +797,10 @@ describe('useForm', () => {
it('form should reset default value when resetting in onSubmit', async () => {
function Comp() {
+ const [asyncDebounceMs, setAsyncDebounceMs] = useState(0)
+
const form = useForm({
+ asyncDebounceMs,
defaultValues: {
name: '',
},
@@ -805,6 +808,7 @@ describe('useForm', () => {
expect(value).toEqual({ name: 'another-test' })
form.reset(value)
+ setAsyncDebounceMs(10)
},
})
As far as I understand, the core of the issue isn't being addressed on this PR yet since invoking formApi.update() with a defaultValues that is different to the current form internal defaultValues will update the internal value overriding any previous change.
I believe the reason for this is to support async initial values however it seems that defaultValues property on useForm should not be reactive if the form internal defaultValues (or values) have ever been updated.
@bpinto, thanks for the input! π
Your first test is failing correctly though isn't it? Essentially your passing in the form's props when the asyncDebounceMs is changed:
{ asyncDebounceMs, defaultValues: { name: '' } }
So expecting "name: another-value" would fail.
I see your line of thinking though, but it doesnβt really match up with my model of how I expect state change. Correct me if I'm wrong, but you're suggesting that form should ignore any future changes made to the default values after a form.reset() is called? I'll run it by the other maintainers though, see what they think. π
I see your line of thinking though, but it doesnβt really match up with my model of how I expect state change. Correct me if I'm wrong, but you're suggesting that form should ignore any future changes made to the default values after a form.reset() is called? I'll run it by the other maintainers though, see what they think. π
π Yep, exactly. I think that form should ignore any future changes made to default values after a form.reset() (or any form value change) but as I said on the beginning of my comment, I'm not sure if my expectation aligns with the library's.
However, I want to emphasize one thing that you are questioning to confirm we are talking about the same thing:
but you're suggesting that form should ignore any future changes made to the default values after a form.reset() is called?
in the tests above, the defaultValues passed to useForm() is not changing, but rather it's only asyncDebounceMs that is changing. The form internal defaultValues is changing because of reset() invocation but the attribute defaultValues that is passed to useForm does not change in the test.
@harry-whorlow I tested out this PR, and I see a UI glitch whenever I reset the form with the new values from the API. The initial values are briefly displayed and then replaced with the new values.
I was running into this same issue. I was submitting the form, awaiting tanstack/query to refetch the data, and then calling formApi.reset(). I was seeing a brief flash of the previous data on reset.
If it helps anyone, calling setTimeout(() => formApi.reset()) (instead of just formApi.reset()) gets rid of the flash. Feels hacky, but it works great. π
@harry-whorlow I tested out this PR, and I see a UI glitch whenever I reset the form with the new values from the API. The initial values are briefly displayed and then replaced
https://github.com/user-attachments/assets/0c849fb8-c1e6-45e7-a9f3-5e403a35723f
with the new values.
I was running into this same issue. I was submitting the form,
awaiting tanstack/query to refetch the data, and then callingformApi.reset(). I was seeing a brief flash of the previous data on reset.If it helps anyone, calling
setTimeout(() => formApi.reset())(instead of justformApi.reset()) gets rid of the flash. Feels hacky, but it works great. π
Confirmed the same issue: See video.
@santiagazo, Hi thanks for the input!
From what I remember jsefiani's flashing bug was unrelated... What version of form is that? did you pull down the pr branch, or the main branch?
[edit]: out of curiosity are you intentionally handling submission in your validators section?
@santiagazo, Hi thanks for the input!
From what I remember jsefiani's flashing bug was unrelated... What version of form is that? did you pull down the pr branch, or the main branch?
[edit]: out of curiosity are you intentionally handling submission in your validators section?
I was running into the same "flashing" behavior on the main branch. I'm currently running @tanstack/react-form v1.12.2.
I also run form submission in my async validator. It's nice because if the backend returns any errors (potentially related to validation), then I can just return them from the validator. That conveniently merges them into the form errors and they all display on the page just like my frontend zod validation does. It probably wasn't the intention when it was designed, but it does work pretty smoothly as long as your backend can return validation errors similar enough to zod (or can be parsed/converted easily enough). π€·ββοΈ
I was running into the same "flashing" behaviour on the main branch. I'm currently running @tanstack/react-form v1.12.2.
@santiagazo ah, you're on the main branch. okay, I thought for a second that you had installed the pr branch. π Have you tried out the pr version?
As a side note, in #1489 we added spreadable error maps so the errors can be spread into the form from the onError of the handle submit. Though, if I remember correctly I think we want to iterate on this.
I fear this PR might have broken reactivity in React. When going from 1.14.0 to 1.14.1, I notice the form state is no longer reactive.
I tried to come up with an example here: https://stackblitz.com/edit/vitejs-vite-o1nrlims
If you look at the code, you'd expect the "Submit" button to change text while it's submitting. But now it no longer works, however if in that stackblitz you try to install v1.14.0, then it works.
The component tree no longer re-renders when submitting, which I expect might be due to the removal to this store hook call.
@RaffaeleCanale that's because this was never intended to be reactive in the first place. It just happened to be this way until now.
If you want to receive reactive data from form state, you should use <form.Subscribe>. The simple example highlights this.
@RaffaeleCanale brother, I'm two days into my holiday π
Jokes aside, thank you for the report and the sandbox it's really appreciated...
As @LeCarbonator has mentioned it was kind of an unintended side effect, something like this should solve your issues: (as per your example)
<form.Subscribe
selector={(state) => state.isSubmitting}
children={(isSubmitting) => (
<button
type="submit"
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-auto px-5 py-2.5 text-center cursor-pointer"
onClick={() => form.handleSubmit()}
>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
)}
/>
Complete sandbox, if you have any more concerns let me know π
You are both right, our code was indeed not properly subscribing to the state and we were blissfully benefiting from the previous behavior, so it should be something we can fix on our side.
Thanks for the clear answer and sorry if I caused a fright, hope you'll still enjoy your holidays :smile:
@RaffaeleCanale no worries, glad to be of help! π