Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/stable-listeners-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/form-core': patch
'@tanstack/form-devtools': patch
---

Refactor: Use stable listener identity instead of index
144 changes: 89 additions & 55 deletions packages/form-core/src/FieldApi/FieldApi.lib.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { batch, createAtom } from '@tanstack/store'
import { callUpdater, createPipelineCache, evaluate, getBy } from '../utils.lib'
import { callUpdater, evaluate, getBy } from '../utils.lib'
import {
clearValidationSourceErrorsFromEvent,
isValidationTriggerEnabled,
Expand All @@ -11,6 +11,7 @@ import {
import { runFieldListenerPipeline } from '../listeners.lib'
import { devtools } from '../devtoolsBridge.lib'
import { reconcileValidatorInstances } from '../ValidatorInstance.lib'
import { reconcileListenerInstances } from '../ListenerInstance.lib'
import { resolveDefaultOptions } from '../defaultOptions.lib'
import {
attachWatchingListenerField,
Expand Down Expand Up @@ -43,7 +44,7 @@ import type {
AnyFieldListener,
FieldListenerTriggers,
} from '../listeners.public'
import type { NameSegment, NameSegments, PipelineCache } from '../utils.lib'
import type { NameSegment, NameSegments } from '../utils.lib'
import type {
FieldValidatorPipelineResult,
PipelineResult,
Expand All @@ -58,6 +59,10 @@ import type {
InternalValidatorInstance,
InternalValidatorInstances,
} from '../ValidatorInstance.lib'
import type {
InternalListenerInstance,
InternalListenerInstances,
} from '../ListenerInstance.lib'
import type { FieldApi, FieldApiOptions } from './FieldApi.public'
import type {
ErrorVisibility,
Expand Down Expand Up @@ -286,19 +291,21 @@ export interface InternalFieldApiParams extends Omit<

export type FieldOptionsScope = 'internal' | 'field'

interface ListenToFieldsMeta {
field: AnyInternalFieldApi
name: string
}

export type FieldWatchingFields = Map<AnyInternalFieldApi, Set<number>>
export type FieldListenToFields = Array<Array<ListenToFieldsMeta>>
export type FieldWatchingListenerFields = Map<
AnyInternalFieldApi,
Set<InternalFieldListenerInstance>
>
export type FieldWatchingValidatorFields = Map<
AnyInternalFieldApi,
Set<InternalFieldValidatorInstance>
>

export type AnyInternalFieldApi = InternalFieldApi<any, any, any>
export type InternalFieldListenerInstance = InternalListenerInstance<
AnyFieldListener,
AnyInternalFieldApi,
AnyInternalFieldApi
>
export type InternalFieldValidatorInstance = InternalValidatorInstance<
AnyFieldValidator,
AnyInternalFieldApi,
Expand All @@ -323,7 +330,12 @@ export class InternalFieldApi<
AnyInternalFieldApi,
AnyInternalFieldApi
>
_listeners: Array<AnyFieldListener> | null
/** Stable runtime instances for this field's listener definitions. */
_listenerInstances: InternalListenerInstances<
AnyFieldListener,
AnyInternalFieldApi,
AnyInternalFieldApi
>
_errorVisibility: ErrorVisibility<any, any> | undefined
_errorBoundary: boolean
/** The form group occupying this trie node. */
Expand All @@ -334,11 +346,8 @@ export class InternalFieldApi<
* @private
* Fields that are listening to this one.
*/
_watchingFields: FieldWatchingFields | null
_listenToFields: FieldListenToFields | null
_watchingListenerFields: FieldWatchingListenerFields | null
_watchingValidatorFields: FieldWatchingValidatorFields | null
/** Lazily allocated runtime state for debounced field listeners. */
_pipelineCache: PipelineCache | null = null
/** Whether this trie node has received usage-site field options. */
_fieldOptionsInitialized: boolean
_isKilled = false
Expand Down Expand Up @@ -412,15 +421,6 @@ export class InternalFieldApi<
return required
}

/** Returns the listener runtime cache, allocating it on first use. */
_getOrCreatePipelineCache(): PipelineCache {
if (!this._pipelineCache) {
this._pipelineCache = createPipelineCache()
}

return this._pipelineCache
}

get atom(): ReadonlyAtom<InternalFieldState> {
return this._getOrCreateAtoms().store
}
Expand Down Expand Up @@ -512,10 +512,17 @@ export class InternalFieldApi<
this._errorVisibility = errorVisibility
this._errorBoundary = errorBoundary ?? false
this._atoms = {}
this._listeners = null
this._watchingFields = null
this._listenToFields = null
this._watchingListenerFields = null
this._watchingValidatorFields = null
this._listenerInstances = reconcileListenerInstances<
AnyFieldListener,
AnyInternalFieldApi,
AnyInternalFieldApi
>({
definitions: listeners,
instances: null,
owner: this,
})
this._validatorInstances = reconcileValidatorInstances<
AnyFieldValidator,
AnyInternalFieldApi,
Expand All @@ -530,14 +537,11 @@ export class InternalFieldApi<

const reconciledListeners = reconcileWatchedListenerFields({
field: this,
prevListenToFields: this._listenToFields,
nextListeners: listeners,
listenerInstances: this._listenerInstances,
form,
})

reconciledListeners.attach.forEach(attachWatchingListenerField)
this._listeners = reconciledListeners.items
this._listenToFields = reconciledListeners.listenToFields

const reconciledValidators = reconcileWatchedValidatorFields({
field: this,
Expand All @@ -563,25 +567,55 @@ export class InternalFieldApi<
this._errorVisibility = resolvedOptions.errorVisibility
this._errorBoundary = resolvedOptions.errorBoundary ?? false

const reconciledListeners = reconcileWatchedListenerFields({
field: this,
prevListenToFields: this._listenToFields,
nextListeners: resolvedOptions.listeners,
form: this.form,
})

reconciledListeners.detach.forEach((operation) =>
detachWatchingListenerField(operation),
)
reconciledListeners.attach.forEach(attachWatchingListenerField)

this._listeners = reconciledListeners.items
this._listenToFields = reconciledListeners.listenToFields
const notifyDependencyChanges = devtools().fieldDependenciesChanged
const dependencyChanges: Array<FieldDependencyChange> | null =
notifyDependencyChanges
? [...reconciledListeners.attach, ...reconciledListeners.detach]
: null
notifyDependencyChanges ? [] : null

if (resolvedOptions.listeners) {
const previousListeners = this._listenerInstances?.map(
(instance) => instance.definition,
)
this._listenerInstances = reconcileListenerInstances<
AnyFieldListener,
AnyInternalFieldApi,
AnyInternalFieldApi
>({
definitions: resolvedOptions.listeners,
previousDefinitions: isInitializing
? undefined
: (previousListeners ?? null),
instances: this._listenerInstances,
owner: this,
onBeforeDispose: (listenerInstance) => {
listenerInstance.resolvedWatchFields?.forEach((sourceField) => {
const operation = {
kind: 'listener' as const,
sourceField,
watchingField: this,
listenerInstance,
}
detachWatchingListenerField(operation)
dependencyChanges?.push(operation)
})
listenerInstance.resolvedWatchFields = null
},
})

const reconciledListeners = reconcileWatchedListenerFields({
field: this,
listenerInstances: this._listenerInstances,
form: this.form,
})

reconciledListeners.detach.forEach((operation) =>
detachWatchingListenerField(operation),
)
reconciledListeners.attach.forEach(attachWatchingListenerField)
dependencyChanges?.push(
...reconciledListeners.attach,
...reconciledListeners.detach,
)
}

if (resolvedOptions.validators) {
const previousValidators = this._validatorInstances?.map(
Expand Down Expand Up @@ -994,7 +1028,7 @@ export class InternalFieldApi<
_notifyListener(
trigger: FieldListenerTriggers,
seenFields: WeakSet<AnyInternalFieldApi>,
onlyRunListenerIndeces: Array<number> | null = null,
onlyRunListenerInstances: ReadonlySet<InternalFieldListenerInstance> | null = null,
) {
if (this._isKilled) return

Expand All @@ -1009,32 +1043,32 @@ export class InternalFieldApi<

seenFields.add(this)

if (this._listeners) {
if (this._listenerInstances) {
runFieldListenerPipeline({
pipeline: this._listeners,
pipeline: this._listenerInstances,
context: {
event: trigger,
fieldApi: this,
formApi: this.form,
},
listenerIndecesToRun: onlyRunListenerIndeces,
listenerInstancesToRun: onlyRunListenerInstances,
})
}

const watchingFields = this._watchingFields
const watchingFields = this._watchingListenerFields
if (!watchingFields) return

for (const [watchingField, listenerIndeces] of watchingFields) {
for (const [watchingField, listenerInstances] of watchingFields) {
if (watchingField._isKilled) {
watchingFields.delete(watchingField)
continue
}

watchingField._notifyListener(trigger, seenFields, [...listenerIndeces])
watchingField._notifyListener(trigger, seenFields, listenerInstances)
}

if (watchingFields.size === 0) {
this._watchingFields = null
this._watchingListenerFields = null
}
}

Expand Down
Loading
Loading