Skip to content

feat(form-core): [v2] expose the previous value to change listeners - #2395

Open
galshir wants to merge 8 commits into
TanStack:alphafrom
galshir:feat/listener-prev-value
Open

galshir wants to merge 8 commits into
TanStack:alphafrom
galshir:feat/listener-prev-value

Conversation

@galshir

@galshir galshir commented Sep 21, 2026 •

Copy link
Copy Markdown

🎯 Changes

Closes #2301.

FormListenerContext and FieldListenerContext now carry an optional prevValue holding the value from immediately before the change that invoked the listener, so a change listener can diff against the value it replaced without keeping its own copy.

  • prevValue is only set for 'change' events; it is undefined for 'blur', 'submit', 'mount', 'reset' and 'unmount'.
  • Form listeners get the previous form values, field listeners get the previous value in their own scope.
  • setFieldValue snapshots _atoms.values before writing and threads that snapshot through _notifyFieldChange → _notifyFormListener / _notifyEvent → _notifyListener.

Each field scope derives its own previous value with getBy(prevFormValues, field.name) rather than receiving the changed field's value. That is what makes the value correct for ancestors and for watchFields listeners: they see what their scope looked like before the change, not the value of the field that triggered it. It also keeps this to one snapshot per mutation, read from an already immutable atom.

Debounced listeners behave the way the issue describes: the pipeline builds a context per change and LiteDebouncer executes the last one, so a listener that fires after five rapid changes sees the fourth value as prevValue and the fifth as value.

filterFieldValues has a branch that notifies a change even when the filtered array is identical; it now passes the current values so prevValue stays defined for every 'change' event.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr.

I could not run pnpm test:pr locally in this environment, so I have left that box unchecked rather than claim it. The tests below were written against the existing specs and I am relying on CI here — happy to iterate on anything it flags.

🚀 Release Impact

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

Tests

Added to packages/form-core/tests/FormApi/listeners.spec.ts and packages/form-core/tests/FieldApi/listeners.spec.ts:

  • the previous value across two consecutive changes, at form and at field level
  • prevValue is undefined on a non-change trigger
  • a debounced listener receives the value from before the change that invokes it
  • an ancestor field receives its own previous object value when a descendant changes

Existing change-listener assertions were updated with the new property; the watched-field cases now assert the listening field's own previous value.

Compatibility

prevValue is a new optional property on the listener context, so existing listeners and their destructuring are unaffected. No other behaviour changes.

Summary by CodeRabbit

  • New Features
    • Form and field change listeners now expose prevValue, showing the value immediately before the change that triggered the listener.
    • Previous values are supported for regular, linked, ancestor, and debounced listeners.
    • prevValue is omitted for non-change events.

@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

Understand this PR’s impact

Explore downstream dependencies and potential security impact with Blast Radius.

View blast radius →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: TanStack/form/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 7591a114-f13b-48dc-949d-8a0bb932309a

📥 Commits

Reviewing files that changed from the base of the PR and between 21591ac and a480960.

📒 Files selected for processing (8)
  • .changeset/brave-moons-listen.md
  • packages/form-core/src/FieldApi/FieldApi.lib.ts
  • packages/form-core/src/FormApi/FormApi.lib.ts
  • packages/form-core/src/FormApi/array-methods.lib.ts
  • packages/form-core/src/listeners.lib.ts
  • packages/form-core/src/listeners.public.ts
  • packages/form-core/tests/FieldApi/listeners.spec.ts
  • packages/form-core/tests/FormApi/listeners.spec.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Changes

Previous listener values

Layer / File(s) Summary
Listener context contracts
packages/form-core/src/listeners.*
Form and field listener contexts now expose optional prevValue fields. Listener pipelines pass these values into listener contexts.
Previous-value capture and notification propagation
packages/form-core/src/FormApi/*, packages/form-core/src/FieldApi/*
Form updates capture prior values and pass them through form, field, ancestor, watcher, and array-field notifications.
Listener behavior validation and release
packages/form-core/tests/*/listeners.spec.ts, .changeset/*
Tests cover direct, non-change, successive, ancestor, watcher, linked, and debounced listener behavior. A minor release note documents the change.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant setFieldValue
  participant _notifyFieldChange
  participant _notifyEvent
  participant runFieldListenerPipeline
  participant runFormListenerPipeline
  setFieldValue->>_notifyFieldChange: Capture and pass previous form values
  _notifyFieldChange->>_notifyEvent: Forward previous form values
  _notifyEvent->>runFieldListenerPipeline: Resolve field prevValue
  _notifyFieldChange->>runFormListenerPipeline: Provide form prevValue
Loading

Suggested reviewers: lecarbonator

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 7 files. (1 skipped: 1 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: exposing the previous value to form-core change listeners.
Description check ✅ Passed The description follows the required template, explains the implementation and behavior, documents tests and compatibility, and includes the required changeset. The local test checklist remains unchec…
Linked Issues check ✅ Passed Issue [#2301] requires optional prevValue on change listener contexts, omission for non-change events, and the value immediately before the change that invokes a debounced listener. `FormListenerCon…
Out of Scope Changes check ✅ Passed The source changes implement [#2301] by capturing and propagating previous values. The listener tests verify the required behavior. The .changeset/brave-moons-listen.md documents the public behavior…
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 7 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

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.

This branch has not been deployed

No deployments
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.

1 participant