Skip to content

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

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

galshir wants to merge 9 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, the value immediately before the change that triggered the listener.
    • Previous values are available to regular, linked, ancestor, and debounced listeners; for debounced listeners, this is the value before the change that invokes the listener.
    • For non-change events, prevValue is undefined.

@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their 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: 94d8697d-1847-4fb9-8ed9-fe7daeb3c58f

📥 Commits

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

📒 Files selected for processing (1)
  • packages/form-core/tests/FieldApi/listeners.spec.ts

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


📝 Walkthrough

Walkthrough

Form and field listener contexts now expose optional prevValue values. Change notifications capture and pass the prior form values through listener pipelines. Tests cover change, non-change, ancestor, linked, watcher, and debounced listeners.

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: Pass previous form values
  _notifyFieldChange->>_notifyEvent: Forward previous form values
  _notifyEvent->>runFieldListenerPipeline: Resolve field prevValue
  _notifyFieldChange->>runFormListenerPipeline: Provide form prevValue
Loading

Suggested reviewers: lecarbonator

Merge Risk: ⚪ Minimal · up to fc318

Listeners receive the value immediately before a change, including in the array-filter notification path. No material regression is established, so the change is mergeable subject to normal CI.

Architecture Summary

Architecture risk: 🔵 Low · up to fc318

The change affects 1 system.

Changed systems: packages/form-core

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — packages/form-core (library) was modified; 7 changed files map to changed impact.

Before / after behavior

  • observed — Modified behavior in packages/form-core/src/FieldApi/FieldApi.lib.ts: _notifyEvent now accepts optional previous form values and passes them through each ancestor’s listener notification.
  • observed — Modified behavior in packages/form-core/src/FieldApi/FieldApi.lib.ts: _notifyListener now accepts previous form values and supplies each listener pipeline with prevValue resolved from the snapshot by field name, or undefined when no snapshot is provided.
  • observed — Modified behavior in packages/form-core/src/FieldApi/FieldApi.lib.ts: Watcher-field listener notifications now receive the same previous-form-values snapshot during recursive propagation.
  • observed — Modified behavior in packages/form-core/src/FormApi/FormApi.lib.ts: setFieldValue now captures the complete form-value state before applying the field update.
🚥 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. 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 is complete and relevant. It explains the change, behavior for change and non-change events, debounced listeners, implementation scope, tests, compatibility, and release impact. It als…
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. The public conte…
Out of Scope Changes check ✅ Passed The source changes support [#2301] by capturing and propagating previous values. The test changes verify the required listener behavior. The changeset documents the public behavior for `@tanstack/form…
  • 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.

…spec

The target field does not change, so its listener receives its own previous value, an empty string, on both calls. This was the one watched-field assertion the earlier commits did not update, and it failed pnpm test:lib.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@galshir

galshir commented Sep 29, 2026

Copy link
Copy Markdown
Author

Ran vitest in packages/form-core on this branch: one existing spec, runs a linked listener when any of multiple watched fields changes, still asserted the listener context without prevValue and failed. fc3181c adds prevValue: '' to both of its assertions. The target field does not change, so its listener receives its own previous value, the empty string.

Now run on this branch: form-core 29 files / 606 tests pass, and tsc and eslint are clean. With form-core built, the react, preact, vue, solid and lit adapter suites pass as well. The angular suite errored locally with Cannot set base providers because it has already been called, which looks like a TestBed setup problem in my environment rather than this change.

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