From 39fb30bcf5497e28471dd70e51191ff88a6ee49c Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Sat, 20 Jun 2026 12:20:36 -0700 Subject: [PATCH] fix(form-core): preserve reset(newValues) when update() is called with stale options When reset(values) is called inside onSubmit, the new values were being overwritten on the next render cycle because useIsomorphicLayoutEffect calls formApi.update(opts) with the original render-closure options. update() compared options.defaultValues (original) to this.options.defaultValues (set by reset), found a difference, and reset the form state back to the original defaults. Fix: add a private _defaultValuesOverridden flag that is set by reset() whenever new explicit values are provided. update() now checks this flag and, when set, skips the shouldUpdateValues logic and preserves the defaultValues that reset() installed. The flag is cleared after one update() cycle so subsequent prop-driven defaultValues changes still work. Fixes #1681 --- packages/form-core/src/FormApi.ts | 22 +++++++++++++++++++ packages/form-core/tests/FormApi.spec.ts | 27 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 5c9de3c1c..2f141532a 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1066,6 +1066,12 @@ export class FormApi< * @private */ private _devtoolsSubmissionOverride: boolean + /** + * @private + * Tracks whether `reset(values)` was called with explicit new default values. + * When true, `update()` must not overwrite those values with stale prop values. + */ + private _defaultValuesOverridden: boolean = false /** * Constructs a new `FormApi` instance with the given form options. @@ -1751,6 +1757,21 @@ export class FormApi< // Options need to be updated first so that when the store is updated, the state is correct for the derived state this.options = options + // If reset(newValues) was called, the incoming `options` still carries the + // original (pre-reset) defaultValues from the render closure. We must not + // let those stale values overwrite what reset() set, so we skip the + // shouldUpdateValues branch and clear the flag for the next render. + if (this._defaultValuesOverridden) { + this._defaultValuesOverridden = false + // Keep the defaultValues that reset() stored on this.options so that + // future renders compare against them correctly. + this.options = { + ...options, + defaultValues: oldOptions.defaultValues, + } + return + } + const shouldUpdateValues = options.defaultValues && !evaluate(options.defaultValues, oldOptions.defaultValues) && @@ -1815,6 +1836,7 @@ export class FormApi< ...this.options, defaultValues: values, } + this._defaultValuesOverridden = true } this.baseStore.setState(() => { diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index 092d0ed6c..2d2b77021 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -144,6 +144,33 @@ describe('form api', () => { form.handleSubmit() }) + it('should not overwrite reset(newValues) when update() is called with stale options afterwards', async () => { + // Simulates the race condition from issue #1681: + // reset(newValues) is called inside onSubmit, then the framework calls + // update(originalOpts) on the next render - the new values must survive. + const originalOptions = { defaultValues: { name: '' } } + const form = new FormApi(originalOptions) + form.mount() + + form.setFieldValue('name', 'test') + await form.handleSubmit() + + // Simulate reset() inside onSubmit having been called with new values + form.reset({ name: 'test' }) + + // State should now reflect the reset values + expect(form.state.values).toEqual({ name: 'test' }) + expect(form.options.defaultValues).toEqual({ name: 'test' }) + + // Simulate the framework calling update() with the original (stale) options + // – this is what useIsomorphicLayoutEffect does after every render + form.update(originalOptions) + + // Values and defaultValues must NOT revert to the original empty string + expect(form.state.values).toEqual({ name: 'test' }) + expect(form.options.defaultValues).toEqual({ name: 'test' }) + }) + it('should reset and set the new default values that are restored after an empty reset', () => { const form = new FormApi({ defaultValues: { name: 'initial' } }) form.mount()