fix: preserve dirty/touched meta on late mount (#5072)#5145
fix: preserve dirty/touched meta on late mount (#5072)#5145
Conversation
When a field component mounts after its value was changed programmatically (e.g., via setFieldValue), the dirty and touched meta flags are now preserved instead of being reset. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 7344bc8 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for vee-validate-docs canceled.
|
✅ Deploy Preview for vee-validate-v5 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Pull request overview
Fixes vee-validate late-mount behavior where programmatic updates (e.g., setFieldValue, setFieldTouched) made before a field component mounts could be lost/overwritten when useField initializes, addressing #5072.
Changes:
- Adjusts field initialization to avoid restaging an initial value when the form already has one, preserving “dirty” on late mount.
- Preserves
touchedwhencreatePathStateis called for a path that already had state created pre-mount. - Adds regression tests covering dirty/touched preservation on late mount and includes a changeset entry.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| packages/vee-validate/src/useFieldState.ts | Skips stageInitialValue in some cases to avoid overwriting initial values and resetting dirty state. |
| packages/vee-validate/src/useForm.ts | Preserves touched when creating path state after a pre-existing state was created programmatically. |
| packages/vee-validate/tests/useForm.spec.ts | Adds regression tests for dirty/touched preservation when fields mount after programmatic updates. |
| .changeset/fix-5072-dirty-meta-lost.md | Adds a patch-level changeset entry for the fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const existingInitial = getFromPath(form.initialValues.value, unref(path)); | ||
| if (existingInitial === undefined || modelValue !== undefined) { | ||
| form.stageInitialValue(unref(path), currentValue, true); | ||
| } |
There was a problem hiding this comment.
existingInitial === undefined can't distinguish between “path missing from initialValues” vs “path present with an explicit initial value of undefined”. In that latter case this will still call stageInitialValue and can overwrite the intended initial state, potentially reintroducing the dirty-meta regression for undefined initial values. Consider checking path presence explicitly (e.g., use getFromPath(..., IS_ABSENT) and compare against the sentinel, plus a dedicated presence check for [nonNested] paths) rather than comparing the resolved value to undefined.
| // Preserve touched state from an existing path state that was created | ||
| // before this field component mounted (e.g., via setFieldValue) (#5072) | ||
| const existingTouched = pathStateExists ? pathStateExists.touched : false; | ||
|
|
||
| const id = FIELD_ID_COUNTER++; | ||
| const state = reactive({ | ||
| id, | ||
| path, | ||
| touched: false, | ||
| touched: existingTouched, |
There was a problem hiding this comment.
In the #5072 scenario, setFieldValue() creates a path state before the field mounts, and then useFieldState() calls createPathState() again on mount. For non-checkbox/radio fields, createPathState() always creates/pushes a new reactive state even when pathStateExists is truthy, leaving the pre-mount state orphaned in pathStates (can’t be removed because it has a different id). Copying touched mitigates one symptom, but the duplicate/orphan state is still a correctness and memory concern and can also drop other programmatically-set meta (e.g. errors/validated). Consider reusing/updating the existing state when one already exists (similar to the checkbox/radio branch), or explicitly removing/replacing the old state before pushing the new one.
| "vee-validate": patch | ||
| --- | ||
|
|
||
| Preserve dirty meta when field component mounts after programmatic changes (#5072) |
There was a problem hiding this comment.
The changeset note mentions preserving dirty meta, but this PR also changes touched preservation on late mount. Consider updating the changeset description to reflect both behaviors so the release note matches the shipped fix.
| Preserve dirty meta when field component mounts after programmatic changes (#5072) | |
| Preserve dirty and touched meta when field component mounts after programmatic changes (#5072) |
Summary
setFieldValuechanges a field's value before the field's host component mounts, the dirty and touched meta flags are now preserved when the component finally mounts anduseFieldrunsuseFieldState.ts: Skip callingstageInitialValuewhen the form already has an initial value for the path and no explicitmodelValuewas provided. This prevents overwriting the original initial value with the current (dirty) value.useForm.ts: IncreatePathState, preserve thetouchedflag from any pre-existing path state (e.g., one created bysetFieldValue) when creating a new path state for the same path.Test plan
🤖 Generated with Claude Code