Skip to content

fix(form-core): establish Field-over-Form prioritization for isDefaultValue and resets#2006

Merged
crutchcorn merged 6 commits into
TanStack:mainfrom
Kyujenius:fix/isDefaultValue-logic
May 10, 2026
Merged

fix(form-core): establish Field-over-Form prioritization for isDefaultValue and resets#2006
crutchcorn merged 6 commits into
TanStack:mainfrom
Kyujenius:fix/isDefaultValue-logic

Conversation

@Kyujenius
Copy link
Copy Markdown
Contributor

@Kyujenius Kyujenius commented Jan 25, 2026

🎯 Changes

Fixes : #1973

This PR is built on the fundamental premise that "The most specific scope (Field) must always take precedence over the generic scope (Form)." Similar to variable shadowing in programming or specificity in CSS, an explicit declaration at the field level represents a stronger developer intent than a global default.

Currently, the lack of a clear hierarchy leads to architectural inconsistencies:

  1. The Paradox: isDefaultValue might claim a value is "default," yet reset() restores a completely different value from the form-level config.
  2. The False Positive: Clearing a field to undefined incorrectly triggers isDefaultValue: true even when explicit defaults are defined at the form level.

By unifying isDefaultValue, form.reset(), and form.resetField() under a single Prioritization Strategy (Field over Form), this PR ensures that TanStack Form behaves as a predictable, high-integrity state machine.

Introduction of Prioritized Default System

This PR introduces a unified prioritization strategy where Field-level defaults always override Form-level defaults.

Key Changes:

  1. isDefaultValue Determination: Now uses a single prioritized default value (Field ?? Form) for comparison instead of a logical OR.
  2. form.reset() Consistency: Now merges form-level defaults with currently mounted field-level defaults, preventing the "reset betrayal" where values would unexpectedly change to form-defaults.
  3. form.resetField() Consistency: Updated to respect the same priority when resetting individual fields.

Fixed Scenarios:

  • Scenario 1: When a default value is specified, setting the field to undefined now correctly sets isDefaultValue: false.
  • Scenario 2: When form-level and field-level defaults differ, both isDefaultValue and reset() now consistently follow the field-level default.

⚠️ I Updated Existing Tests

I’d like to verify if this matches what the maintainers had in mind.

Modified current test cases in FieldApi.spec.ts that were previously based on the incorrect logical OR assumption. These tests were updated to reflect the new prioritized logic, ensuring that isDefaultValue only returns true for the actual value that form.reset() would restore.

isDefaultValue Check Logic AS-IS / TO-BE

AS-IS TO-BE
image image

resset() Logic AS-IS / TO-BE

AS-IS TO-BE
image image

IMO

I realize this shifts the precedence closer to a 'Variable Shadowing' model. If this is too big of a breaking change, I'm happy to discuss putting this behind a flag or finding a middle ground. My main goal is to ensure reset() and isDefaultValue rely on the same logic. I believe this prioritization makes the library significantly more predictable. By aligning the internal logic with how developers naturally think about scope and specificity,

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr. (Verified form-core tests and custom regression tests)

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • New Features

    • Introduced Prioritized Default System that enforces consistent field-level default handling throughout form operations. Field-specific defaults now take precedence over form-wide defaults when resetting forms and deriving field state.
  • Bug Fixes

    • Fixed improper default value detection when explicit values are specified, improving accuracy and reliability of default state tracking.

Review Change Stack

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jan 25, 2026

🦋 Changeset detected

Latest commit: 699134c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@tanstack/form-core Minor
@tanstack/angular-form Minor
@tanstack/form-devtools Patch
@tanstack/lit-form Patch
@tanstack/react-form Minor
@tanstack/solid-form Minor
@tanstack/svelte-form Minor
@tanstack/vue-form Minor
@tanstack/react-form-devtools Patch
@tanstack/solid-form-devtools Patch
@tanstack/react-form-nextjs Minor
@tanstack/react-form-remix Minor
@tanstack/react-form-start Minor

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

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Jan 25, 2026

View your CI Pipeline Execution ↗ for commit 51d5019

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 1m 23s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 38s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-10 19:27:46 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jan 25, 2026

More templates

@tanstack/angular-form

npm i https://pkg.pr.new/@tanstack/angular-form@2006

@tanstack/form-core

npm i https://pkg.pr.new/@tanstack/form-core@2006

@tanstack/form-devtools

npm i https://pkg.pr.new/@tanstack/form-devtools@2006

@tanstack/lit-form

npm i https://pkg.pr.new/@tanstack/lit-form@2006

@tanstack/preact-form

npm i https://pkg.pr.new/@tanstack/preact-form@2006

@tanstack/react-form

npm i https://pkg.pr.new/@tanstack/react-form@2006

@tanstack/react-form-devtools

npm i https://pkg.pr.new/@tanstack/react-form-devtools@2006

@tanstack/react-form-nextjs

npm i https://pkg.pr.new/@tanstack/react-form-nextjs@2006

@tanstack/react-form-remix

npm i https://pkg.pr.new/@tanstack/react-form-remix@2006

@tanstack/react-form-start

npm i https://pkg.pr.new/@tanstack/react-form-start@2006

@tanstack/solid-form

npm i https://pkg.pr.new/@tanstack/solid-form@2006

@tanstack/solid-form-devtools

npm i https://pkg.pr.new/@tanstack/solid-form-devtools@2006

@tanstack/svelte-form

npm i https://pkg.pr.new/@tanstack/svelte-form@2006

@tanstack/vue-form

npm i https://pkg.pr.new/@tanstack/vue-form@2006

commit: 51d5019

@Kyujenius Kyujenius force-pushed the fix/isDefaultValue-logic branch from 3253468 to 9dead10 Compare January 25, 2026 16:29
@sentry
Copy link
Copy Markdown

sentry Bot commented Jan 25, 2026

Codecov Report

❌ Patch coverage is 94.73684% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.76%. Comparing base (6892ed0) to head (51d5019).
⚠️ Report is 197 commits behind head on main.

Files with missing lines Patch % Lines
packages/form-core/src/FormApi.ts 94.73% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2006      +/-   ##
==========================================
+ Coverage   90.35%   90.76%   +0.41%     
==========================================
  Files          38       59      +21     
  Lines        1752     2209     +457     
  Branches      444      555     +111     
==========================================
+ Hits         1583     2005     +422     
- Misses        149      183      +34     
- Partials       20       21       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@LeCarbonator LeCarbonator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks clean! I'll need to do some additional checks though, since this is potentially a breaking change instead of a fix.

It may take a few days. Thanks for tackling the issue!

Comment thread packages/form-core/src/FormApi.ts Outdated

field.setValue('test')
expect(field.getMeta().isDefaultValue).toBe(true)
expect(field.getMeta().isDefaultValue).toBe(false)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder to self: Check git blame.

I don't this change is good, but I want to know the context of why it was explicitly listed as unit test.

Copy link
Copy Markdown
Contributor Author

@Kyujenius Kyujenius Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LeCarbonator Thanks for taking the time to review this!

I checked the git blame - this test was intentionally written this way in PR #1456 It wasn't a mistake, so I want to be careful here.

But I think there's a philosophical question: What should "default" mean when form-level and field-level disagree?

The Two Interpretations

Original (OR logic): "Both are defaults"

isDefaultValue = matches(formDefault) || matches(fieldDefault)

Proposed (?? logic): "The one reset() uses is THE default"

isDefaultValue = matches(fieldDefault ?? formDefault)

Why I Lean Toward the Proposed Logic

In #1081, explained the purpose of isDefaultValue:

"if you do something like !meta.isDefaultValue will give you the react ecosystem's isDirty"

RHF's isDirty is true when currentValue !== defaultValue. A form has exactly one clean state.

With the original OR logic:

  • isDefaultValue is true for both 'test' and 'another-test'
  • So !isDefaultValue is false for both values
  • This means the form has two clean states

That feels inconsistent with RHF's model. A form should have one default state, not two.

The Practical Problem

// User checks before reset
if (!field.getMeta().isDefaultValue) {
  form.reset()  // "I'm not at default, let me reset"
}

With OR logic, if value is 'test' (form-level default):

  • isDefaultValuetrue → user skips reset
  • But reset() would actually change the value to 'another-test'

The user gets misleading information.

I Could Be Wrong

I understand the original design might have had reasons I'm not aware of. Maybe there are use cases where treating both as "default" makes sense. What's your take on this?

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

📝 Walkthrough

Walkthrough

This PR implements a prioritized default-value system for TanStack Form, enforcing field-level defaults over form-level defaults across isDefaultValue derivation, form.reset(), and form.resetField() operations, with corrected handling to prevent undefined from being incorrectly treated as a default value.

Changes

Prioritized Default System

Layer / File(s) Summary
Feature Declaration
.changeset/gentle-jars-share.md
Announces minor version bump and documents the new prioritized default-value system for isDefaultValue, form.reset(), and form.resetField().
Default Value Derivation
packages/form-core/src/FormApi.ts
Refines fieldMeta.isDefaultValue computation to use a single prioritized default source (field instance defaultValue fallback to form defaultValues) via evaluate helper.
Reset and ResetField Implementation
packages/form-core/src/FormApi.ts
Updates reset() to compute nextValues from each field instance's defaultValue (when defined), falling back to form defaults; updates resetField() to apply a prioritized targetValue and conditionally write only when not undefined.
Test Coverage
packages/form-core/tests/FieldApi.spec.ts, packages/form-core/tests/FormApi.spec.ts
Corrects existing isDefaultValue assertion; adds tests for falsy primitive defaults, undefined handling, and form.reset() behavior with field-level default prioritization.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A form finds clarity at last,
Fields now guard their defaults so fast,
No more form rules muddying the way,
Each field's choice wins the day!
Reset prioritizes with care,
Default harmony everywhere. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: establishing Field-over-Form prioritization for isDefaultValue and resets, which directly matches the primary objective of the changeset.
Description check ✅ Passed The description comprehensively covers the changes, motivation, and impact. It includes completed checklist items, a generated changeset, detailed explanation of the prioritization strategy, test updates, and visual flowcharts showing the before/after logic.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/gentle-jars-share.md:
- Around line 2-5: The changeset claims a minor bump but introduces behavioral
precedence altering user-visible semantics (Prioritized Default System)
affecting isDefaultValue, form.reset(), and form.resetField(); re-evaluate and
change the changeset to a major version bump or gate the behavior behind a
feature flag/opt-in, update the .changeset text to reflect the decision, and
ensure release notes explicitly document the behavioral change and migration
guidance for consumers relying on previous dual-default behavior.

In `@packages/form-core/src/FormApi.ts`:
- Around line 1135-1140: The code uses nullish coalescing (??) so an explicit
field default of null is treated as missing and falls back to form defaults;
update the logic in the evaluate call to treat only undefined as "missing"
(preserve null/false/0 as valid defaults). Concretely, retrieve the
field-specific default via
this.getFieldInfo(fieldName)?.instance?.options.defaultValue into a local (e.g.,
fieldDefault) and use fieldDefault !== undefined ? fieldDefault :
getBy(this.options.defaultValues, fieldName) when calling evaluate (change the
occurrence inside evaluate and the similar occurrences around the 2601-2605
area) so explicit null values retain precedence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b1d96c6d-8ef2-4650-b68b-d363a041a5ea

📥 Commits

Reviewing files that changed from the base of the PR and between cab9db4 and 51d5019.

📒 Files selected for processing (4)
  • .changeset/gentle-jars-share.md
  • packages/form-core/src/FormApi.ts
  • packages/form-core/tests/FieldApi.spec.ts
  • packages/form-core/tests/FormApi.spec.ts

Comment on lines +2 to +5
'@tanstack/form-core': minor
---

Introduced a **Prioritized Default System** that ensures consistency between field metadata and form reset behavior. This change prioritizes field-level default values over form-level defaults across `isDefaultValue` derivation, `form.reset()`, and `form.resetField()`. This ensures that field metadata accurately reflects the state the form would return to upon reset and prevents `undefined` from being incorrectly treated as a default when a value is explicitly specified.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Recheck semver level for this behavioral precedence change.

This changes user-visible semantics of isDefaultValue, reset(), and resetField(); consumers depending on previous dual-default behavior may break. Please confirm whether this should be a major bump (or feature-flagged), not minor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/gentle-jars-share.md around lines 2 - 5, The changeset claims a
minor bump but introduces behavioral precedence altering user-visible semantics
(Prioritized Default System) affecting isDefaultValue, form.reset(), and
form.resetField(); re-evaluate and change the changeset to a major version bump
or gate the behavior behind a feature flag/opt-in, update the .changeset text to
reflect the decision, and ensure release notes explicitly document the
behavioral change and migration guidance for consumers relying on previous
dual-default behavior.

Comment on lines +1135 to +1140
const isDefaultValue = evaluate(
curFieldVal,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
this.getFieldInfo(fieldName)?.instance?.options.defaultValue ??
getBy(this.options.defaultValues, fieldName),
) ||
evaluate(
curFieldVal,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
this.getFieldInfo(fieldName)?.instance?.options.defaultValue,
)
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

null field defaults lose precedence due to ??.

Using nullish coalescing here makes explicit defaultValue: null fall back to form defaults, which breaks Field-over-Form priority and can desynchronize reset/default metadata behavior.

Suggested fix
-const isDefaultValue = evaluate(
-  curFieldVal,
-  this.getFieldInfo(fieldName)?.instance?.options.defaultValue ??
-    getBy(this.options.defaultValues, fieldName),
-)
+const fieldDefaultValue =
+  this.getFieldInfo(fieldName).instance?.options.defaultValue
+const prioritizedDefaultValue =
+  fieldDefaultValue !== undefined
+    ? fieldDefaultValue
+    : getBy(this.options.defaultValues, fieldName)
+const isDefaultValue = evaluate(curFieldVal, prioritizedDefaultValue)
-const fieldDefault =
-  this.getFieldInfo(field).instance?.options.defaultValue
-const formDefault = getBy(this.options.defaultValues, field)
-const targetValue = fieldDefault ?? formDefault
+const fieldDefault =
+  this.getFieldInfo(field).instance?.options.defaultValue
+const formDefault = getBy(this.options.defaultValues, field)
+const targetValue = fieldDefault !== undefined ? fieldDefault : formDefault

Also applies to: 2601-2605

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/form-core/src/FormApi.ts` around lines 1135 - 1140, The code uses
nullish coalescing (??) so an explicit field default of null is treated as
missing and falls back to form defaults; update the logic in the evaluate call
to treat only undefined as "missing" (preserve null/false/0 as valid defaults).
Concretely, retrieve the field-specific default via
this.getFieldInfo(fieldName)?.instance?.options.defaultValue into a local (e.g.,
fieldDefault) and use fieldDefault !== undefined ? fieldDefault :
getBy(this.options.defaultValues, fieldName) when calling evaluate (change the
occurrence inside evaluate and the similar occurrences around the 2601-2605
area) so explicit null values retain precedence.

Copy link
Copy Markdown
Member

@crutchcorn crutchcorn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really good. Thank you for the thoughtful PR and description; sorry that it took me so long to review + merge.

Just as a heads up, @LeCarbonator is considering some changes to defaultValue in a field for v2. Namely removing it outright. Would love to hear your feedback on that idea as someone who's clearly thought about field-first default values.

@crutchcorn crutchcorn merged commit 556e35e into TanStack:main May 10, 2026
9 checks passed
@github-actions github-actions Bot mentioned this pull request May 10, 2026
@Kyujenius
Copy link
Copy Markdown
Contributor Author

Kyujenius commented May 11, 2026

@crutchcorn Thanks so much, and no worries on the timing!

I’ve picked up on bits of the related conversations around field-level vs form-level defaults, so dropping field-level defaultValue in v2 sounds like a reasonable direction from where I’m sitting. Would love to contribute to that effort in whatever capacity is useful — just let me know where the discussion lives and I’ll join in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

isDefaultValue is true when field value is undefined, and also for two distinct values

3 participants