From 129464807bf880effb4981da84c986536408ff64 Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:24:26 +0200 Subject: [PATCH 1/5] refactor(react): make react components a subclass --- .../react-form/src/AppForm/Components.lib.tsx | 21 +++------ .../src/AppForm/initializeAppForm.lib.ts | 9 ++-- .../src/ReactForm/Components.lib.tsx | 41 +++------------- .../src/ReactForm/ReactFormApi.lib.tsx | 47 +++++++++++++++---- 4 files changed, 56 insertions(+), 62 deletions(-) diff --git a/packages/react-form/src/AppForm/Components.lib.tsx b/packages/react-form/src/AppForm/Components.lib.tsx index ac18641cd..bc36b9a86 100644 --- a/packages/react-form/src/AppForm/Components.lib.tsx +++ b/packages/react-form/src/AppForm/Components.lib.tsx @@ -1,18 +1,12 @@ import React from 'react' import { InternalFormGroupApi } from '@tanstack/form-core/internals' -import { - attachReactFormComponents, - createArrayFieldComponent, -} from '../ReactForm/Components.lib' +import { createArrayFieldComponent } from '../ReactForm/Components.lib' import { useField } from '../ReactForm/useField.lib' import { useValueFieldSubscription } from '../ReactForm/fieldSubscriptions.lib' import { Subscribe } from '../Subscribe.public' import { FieldContext, FormContext } from './contexts.lib' -import type { - AnyInternalFormApi, - FieldOptionsScope, -} from '@tanstack/form-core/internals' +import type { FieldOptionsScope } from '@tanstack/form-core/internals' import type { FunctionComponent, ReactNode } from 'react' import type { AppFormComponent, @@ -28,14 +22,11 @@ import type { InternalReactFormApi } from '../ReactForm/ReactFormApi.lib' type AnyReactAppFormApi = ReactAppFormApi export function attachReactAppFormComponents( - form: AnyInternalFormApi, + form: InternalReactFormApi, formComponents: Record>, fieldComponents: Record>, ): AnyReactAppFormApi { - const resultForm = attachReactFormComponents( - form, - fieldComponents, - ) as never as AnyReactAppFormApi + const resultForm = form as never as AnyReactAppFormApi const field = createFieldWithContext(form, fieldComponents, 'field') const arrayField = resultForm.ArrayField as FunctionComponent const groupField = createFieldWithContext(form, fieldComponents, 'field') @@ -58,7 +49,7 @@ export function attachReactAppFormComponents( return Object.assign(resultForm, formComponents) } -function createAppForm(form: AnyInternalFormApi): AppFormComponent { +function createAppForm(form: InternalReactFormApi): AppFormComponent { const AppForm: FunctionComponent<{ children: Exclude> }> = function AppFormComponent(props) { @@ -76,7 +67,7 @@ type AnyFieldComponent = FunctionComponent< > function createFieldWithContext( - form: AnyInternalFormApi, + form: InternalReactFormApi, fieldComponents: Record>, scope: FieldOptionsScope, ) { diff --git a/packages/react-form/src/AppForm/initializeAppForm.lib.ts b/packages/react-form/src/AppForm/initializeAppForm.lib.ts index b8bbdca8a..654d23518 100644 --- a/packages/react-form/src/AppForm/initializeAppForm.lib.ts +++ b/packages/react-form/src/AppForm/initializeAppForm.lib.ts @@ -1,4 +1,4 @@ -import { InternalFormApi } from '@tanstack/form-core/internals' +import { InternalReactFormApi } from '../ReactForm/ReactFormApi.lib' import { attachReactAppFormComponents } from './Components.lib' import type { DefaultFieldOptions, @@ -7,7 +7,6 @@ import type { DefaultOptions, FormOptions, } from '@tanstack/form-core' -import type { InternalReactFormApi } from '../ReactForm/ReactFormApi.lib' import type { FunctionComponent } from 'react' interface AnyCreateFormHookOptions { @@ -35,7 +34,11 @@ export function createAppFormInitializer( : undefined return (options) => { - const form = new InternalFormApi(options, defaultOptions) + const form = new InternalReactFormApi( + options, + defaultOptions, + createOptions.fieldComponents, + ) const extendedForm = attachReactAppFormComponents( form, createOptions.formComponents, diff --git a/packages/react-form/src/ReactForm/Components.lib.tsx b/packages/react-form/src/ReactForm/Components.lib.tsx index 48e70df5c..18d9ae432 100644 --- a/packages/react-form/src/ReactForm/Components.lib.tsx +++ b/packages/react-form/src/ReactForm/Components.lib.tsx @@ -6,10 +6,7 @@ import { useValueFieldSubscription, } from './fieldSubscriptions.lib' import { useField } from './useField.lib' -import type { - AnyInternalFormApi, - FieldOptionsScope, -} from '@tanstack/form-core/internals' +import type { FieldOptionsScope } from '@tanstack/form-core/internals' import type { InternalReactFormApi } from './ReactFormApi.lib' import type { FunctionComponent, ReactNode } from 'react' import type { @@ -18,36 +15,12 @@ import type { ReactFormSubscribeProps, } from './Components.public' -export function attachReactFormComponents( - form: AnyInternalFormApi, - fieldComponents: Record> | null, -): InternalReactFormApi { - const resultForm = form as InternalReactFormApi - resultForm.Field = createFieldComponent( - form, - fieldComponents, - 'field', - ) as InternalReactFormApi['Field'] - resultForm.ArrayField = createArrayFieldComponent( - form, - fieldComponents, - 'field', - ) - resultForm.Subscribe = createSubscribeComponent(form) - resultForm.FormGroup = createFormGroupComponent( - resultForm, - fieldComponents, - ) as InternalReactFormApi['FormGroup'] - - return resultForm -} - type AnyFieldComponent = FunctionComponent< ReactFormFieldProps > -function createFieldComponent( - form: AnyInternalFormApi, +export function createFieldComponent( + form: InternalReactFormApi, fieldComponents: Record> | null, scope: FieldOptionsScope, ): AnyFieldComponent { @@ -69,7 +42,7 @@ type AnyArrayFieldComponent = { } export function createArrayFieldComponent( - form: AnyInternalFormApi, + form: InternalReactFormApi, fieldComponents: Record> | null, scope: FieldOptionsScope, ): AnyArrayFieldComponent { @@ -90,8 +63,8 @@ type AnySubscribeComponent = { displayName?: string } -function createSubscribeComponent( - form: AnyInternalFormApi, +export function createSubscribeComponent( + form: InternalReactFormApi, ): AnySubscribeComponent { const TanStackFormSubscribe: AnySubscribeComponent = (props) => { return @@ -106,7 +79,7 @@ type AnyFormGroupComponent = FunctionComponent< ReactFormGroupProps > -function createFormGroupComponent( +export function createFormGroupComponent( form: InternalReactFormApi, fieldComponents: Record> | null, ): AnyFormGroupComponent { diff --git a/packages/react-form/src/ReactForm/ReactFormApi.lib.tsx b/packages/react-form/src/ReactForm/ReactFormApi.lib.tsx index 1236ed81f..e2bedfcfc 100644 --- a/packages/react-form/src/ReactForm/ReactFormApi.lib.tsx +++ b/packages/react-form/src/ReactForm/ReactFormApi.lib.tsx @@ -1,25 +1,52 @@ import { InternalFormApi } from '@tanstack/form-core/internals' import * as React from 'react' import { useEffect, useRef } from 'react' -import { attachReactFormComponents } from './Components.lib' -import type { AnyInternalFormApi } from '@tanstack/form-core/internals' -import type { FormOptions } from '@tanstack/form-core' +import { + createArrayFieldComponent, + createFieldComponent, + createFormGroupComponent, + createSubscribeComponent, +} from './Components.lib' +import type { DefaultOptions, FormOptions } from '@tanstack/form-core' +import type { FunctionComponent } from 'react' import type { ReactTanStackFormComponents } from './Components.public' const useReactId = (React as typeof React & { useId?: () => string }).useId ?? (() => undefined) -export interface InternalReactFormApi - extends AnyInternalFormApi, ReactTanStackFormComponents {} +export class InternalReactFormApi + extends InternalFormApi + implements ReactTanStackFormComponents +{ + Field: ReactTanStackFormComponents['Field'] + ArrayField: ReactTanStackFormComponents['ArrayField'] + Subscribe: ReactTanStackFormComponents['Subscribe'] + FormGroup: ReactTanStackFormComponents['FormGroup'] + + constructor( + options: FormOptions, + defaultOptions?: DefaultOptions, + fieldComponents: Record> | null = null, + ) { + super(options, defaultOptions) + this.Field = createFieldComponent( + this, + fieldComponents, + 'field', + ) as InternalReactFormApi['Field'] + this.ArrayField = createArrayFieldComponent(this, fieldComponents, 'field') + this.Subscribe = createSubscribeComponent(this) + this.FormGroup = createFormGroupComponent( + this, + fieldComponents, + ) as InternalReactFormApi['FormGroup'] + } +} export function initializeForm( options: FormOptions, ): InternalReactFormApi { - const form = new InternalFormApi(options) - - const reactFormApi = attachReactFormComponents(form, null) - - return reactFormApi + return new InternalReactFormApi(options) } export function useInternalForm( From ff179f4f84e9f10035f3b8fa04261708f546524a Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:14:57 +0200 Subject: [PATCH 2/5] refactor: move react app form to class impl --- .../react-form/src/AppForm/Components.lib.tsx | 43 ++----------- .../src/AppForm/ReactAppFormApi.lib.ts | 64 +++++++++++++++++++ .../src/AppForm/initializeAppForm.lib.ts | 21 ++---- .../react-form/tests/createFormHook.spec.tsx | 49 +++++++++++++- 4 files changed, 123 insertions(+), 54 deletions(-) create mode 100644 packages/react-form/src/AppForm/ReactAppFormApi.lib.ts diff --git a/packages/react-form/src/AppForm/Components.lib.tsx b/packages/react-form/src/AppForm/Components.lib.tsx index bc36b9a86..c5980984e 100644 --- a/packages/react-form/src/AppForm/Components.lib.tsx +++ b/packages/react-form/src/AppForm/Components.lib.tsx @@ -1,55 +1,20 @@ import React from 'react' import { InternalFormGroupApi } from '@tanstack/form-core/internals' -import { createArrayFieldComponent } from '../ReactForm/Components.lib' import { useField } from '../ReactForm/useField.lib' import { useValueFieldSubscription } from '../ReactForm/fieldSubscriptions.lib' import { Subscribe } from '../Subscribe.public' import { FieldContext, FormContext } from './contexts.lib' import type { FieldOptionsScope } from '@tanstack/form-core/internals' import type { FunctionComponent, ReactNode } from 'react' -import type { - AppFormComponent, - ReactAppFormApi, -} from './ReactAppFormApi.public' -import type { AnyReactFormComponentMap } from './componentMap.public' +import type { AppFormComponent } from './ReactAppFormApi.public' import type { ReactFormFieldProps, ReactFormGroupProps, } from '../ReactForm/Components.public' import type { InternalReactFormApi } from '../ReactForm/ReactFormApi.lib' -type AnyReactAppFormApi = ReactAppFormApi - -export function attachReactAppFormComponents( - form: InternalReactFormApi, - formComponents: Record>, - fieldComponents: Record>, -): AnyReactAppFormApi { - const resultForm = form as never as AnyReactAppFormApi - const field = createFieldWithContext(form, fieldComponents, 'field') - const arrayField = resultForm.ArrayField as FunctionComponent - const groupField = createFieldWithContext(form, fieldComponents, 'field') - const groupArrayField = createArrayFieldComponent( - form, - fieldComponents, - 'field', - ) as FunctionComponent - const formGroup = createFormGroupWithContext( - resultForm as any, - groupField, - groupArrayField, - ) - - resultForm.AppForm = createAppForm(form) - resultForm.Field = field as AnyReactAppFormApi['Field'] - resultForm.ArrayField = arrayField as AnyReactAppFormApi['ArrayField'] - resultForm.FormGroup = formGroup as AnyReactAppFormApi['FormGroup'] - - return Object.assign(resultForm, formComponents) -} - -function createAppForm(form: InternalReactFormApi): AppFormComponent { +export function createAppForm(form: InternalReactFormApi): AppFormComponent { const AppForm: FunctionComponent<{ children: Exclude> }> = function AppFormComponent(props) { @@ -66,7 +31,7 @@ type AnyFieldComponent = FunctionComponent< ReactFormFieldProps > -function createFieldWithContext( +export function createFieldWithContext( form: InternalReactFormApi, fieldComponents: Record>, scope: FieldOptionsScope, @@ -92,7 +57,7 @@ type AnyFormGroupComponent = FunctionComponent< ReactFormGroupProps > -function createFormGroupWithContext( +export function createFormGroupWithContext( form: InternalReactFormApi, groupField: FunctionComponent, groupArrayField: FunctionComponent, diff --git a/packages/react-form/src/AppForm/ReactAppFormApi.lib.ts b/packages/react-form/src/AppForm/ReactAppFormApi.lib.ts new file mode 100644 index 000000000..ece94c66b --- /dev/null +++ b/packages/react-form/src/AppForm/ReactAppFormApi.lib.ts @@ -0,0 +1,64 @@ +import { createArrayFieldComponent } from '../ReactForm/Components.lib' +import { InternalReactFormApi } from '../ReactForm/ReactFormApi.lib' +import { + createAppForm, + createFieldWithContext, + createFormGroupWithContext, +} from './Components.lib' +import type { DefaultOptions, FormOptions } from '@tanstack/form-core' +import type { FunctionComponent } from 'react' +import type { AppFormComponent } from './ReactAppFormApi.public' + +type FormComponents = Record> + +export interface InternalReactAppFormApiInstance extends InternalReactFormApi { + AppForm: AppFormComponent +} + +type InternalReactAppFormApiConstructor< + TFormComponents extends FormComponents, +> = new ( + options: FormOptions, + defaultOptions?: DefaultOptions, +) => InternalReactAppFormApiInstance & TFormComponents + +export function createInternalReactAppFormApiClass< + const TFormComponents extends FormComponents, +>( + formComponents: TFormComponents, + fieldComponents: FormComponents, +): InternalReactAppFormApiConstructor { + class InternalReactAppFormApi + extends InternalReactFormApi + implements InternalReactAppFormApiInstance + { + AppForm: AppFormComponent + + constructor( + options: FormOptions, + defaultOptions?: DefaultOptions, + ) { + super(options, defaultOptions, fieldComponents) + + const field = createFieldWithContext(this, fieldComponents, 'field') + const groupField = createFieldWithContext(this, fieldComponents, 'field') + const groupArrayField = createArrayFieldComponent( + this, + fieldComponents, + 'field', + ) as FunctionComponent + + this.AppForm = createAppForm(this) + this.Field = field as InternalReactAppFormApi['Field'] + this.FormGroup = createFormGroupWithContext( + this, + groupField, + groupArrayField, + ) as InternalReactAppFormApi['FormGroup'] + } + } + + Object.assign(InternalReactAppFormApi.prototype, formComponents) + + return InternalReactAppFormApi as unknown as InternalReactAppFormApiConstructor +} diff --git a/packages/react-form/src/AppForm/initializeAppForm.lib.ts b/packages/react-form/src/AppForm/initializeAppForm.lib.ts index 654d23518..b379feb5e 100644 --- a/packages/react-form/src/AppForm/initializeAppForm.lib.ts +++ b/packages/react-form/src/AppForm/initializeAppForm.lib.ts @@ -1,5 +1,5 @@ -import { InternalReactFormApi } from '../ReactForm/ReactFormApi.lib' -import { attachReactAppFormComponents } from './Components.lib' +import { createInternalReactAppFormApiClass } from './ReactAppFormApi.lib' +import type { InternalReactFormApi } from '../ReactForm/ReactFormApi.lib' import type { DefaultFieldOptions, DefaultFormGroupOptions, @@ -20,6 +20,10 @@ interface AnyCreateFormHookOptions { export function createAppFormInitializer( createOptions: AnyCreateFormHookOptions, ): (options: FormOptions) => InternalReactFormApi { + const InternalReactAppFormApi = createInternalReactAppFormApiClass( + createOptions.formComponents, + createOptions.fieldComponents, + ) const hasDefaultOptions = createOptions.defaultFormOptions || createOptions.defaultFieldOptions || @@ -34,17 +38,6 @@ export function createAppFormInitializer( : undefined return (options) => { - const form = new InternalReactFormApi( - options, - defaultOptions, - createOptions.fieldComponents, - ) - const extendedForm = attachReactAppFormComponents( - form, - createOptions.formComponents, - createOptions.fieldComponents, - ) - - return extendedForm as never + return new InternalReactAppFormApi(options, defaultOptions) } } diff --git a/packages/react-form/tests/createFormHook.spec.tsx b/packages/react-form/tests/createFormHook.spec.tsx index 2e6481291..0edff7402 100644 --- a/packages/react-form/tests/createFormHook.spec.tsx +++ b/packages/react-form/tests/createFormHook.spec.tsx @@ -7,7 +7,54 @@ import type { AnyInternalFormApi, } from '@tanstack/form-core/internals' -describe('createFormHook defaults', () => { +describe('createFormHook', () => { + it('supports destructuring registered form components', () => { + const SharedComponent = () => Shared component + const { useAppForm } = createFormHook({ + fieldComponents: {}, + formComponents: { SharedComponent }, + }) + + /* eslint-disable @eslint-react/static-components -- False positive, component's created once */ + function Component() { + const form = useAppForm({ defaultValues: { name: '' } }) + const { SharedComponent: DestructuredComponent } = form + + return + } + /* eslint-enable @eslint-react/static-components */ + + const { getByText } = render() + + expect(getByText('Shared component')).toBeInTheDocument() + }) + + it('renders inherited form components with per-instance AppForm context', () => { + function CurrentName() { + const form = useFormContext() + return {form.state.values.name} + } + + const { useAppForm, useFormContext } = createFormHook({ + fieldComponents: {}, + formComponents: { CurrentName }, + }) + + function Component() { + const form = useAppForm({ defaultValues: { name: 'Tony' } }) + + return ( + + + + ) + } + + const { getByTestId } = render() + + expect(getByTestId('current-name')).toHaveTextContent('Tony') + }) + it('uses default form options and lets usage options override them', () => { const defaultErrorVisibility = () => true const overriddenErrorVisibility = () => false From 158e06fc8aff6cf8c4e515ce3b9ed0ecd73f534e Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:02:57 +0200 Subject: [PATCH 3/5] refactor: move field API call to form prop --- packages/form-core/src/FieldApi/FieldApi.lib.ts | 2 +- packages/form-core/src/FormApi/FormApi.lib.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/form-core/src/FieldApi/FieldApi.lib.ts b/packages/form-core/src/FieldApi/FieldApi.lib.ts index 36e276e59..127e52ff0 100644 --- a/packages/form-core/src/FieldApi/FieldApi.lib.ts +++ b/packages/form-core/src/FieldApi/FieldApi.lib.ts @@ -232,7 +232,7 @@ export function getOrCreateFieldApi( return getOrCreateFieldApi(childNode, segments, form, options, scope) } - childNode = new InternalFieldApi( + childNode = new form._FieldApi( { segment, parent: node, diff --git a/packages/form-core/src/FormApi/FormApi.lib.ts b/packages/form-core/src/FormApi/FormApi.lib.ts index 8a8575788..c73729e2c 100644 --- a/packages/form-core/src/FormApi/FormApi.lib.ts +++ b/packages/form-core/src/FormApi/FormApi.lib.ts @@ -1,5 +1,6 @@ import { batch, createAtom } from '@tanstack/store' import { + InternalFieldApi, getDefaultValueCacheResult, getOrCreateFieldApi, shouldCacheDefaultValue, @@ -212,6 +213,8 @@ export class InternalFormApi< */ static majorVersion = 2 + // Allows adapters to control which InternalFieldApi subclass creates fields. + _FieldApi = InternalFieldApi atom: ReadonlyAtom< FormState> > From b62141c228feed94417f719fdd05ec39e303592c Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:38:05 +0200 Subject: [PATCH 4/5] move field components to prototype --- packages/form-core/src/FormApi/FormApi.lib.ts | 4 ++- .../react-form/src/AppForm/Components.lib.tsx | 3 +- .../src/AppForm/ReactAppFieldApi.lib.ts | 14 ++++++++ .../src/AppForm/ReactAppFormApi.lib.ts | 15 +++++--- .../src/ReactForm/Components.lib.tsx | 17 +++------- .../src/ReactForm/ReactFormApi.lib.tsx | 6 +--- .../react-form/src/ReactForm/useField.lib.ts | 7 +--- .../react-form/tests/createFormHook.spec.tsx | 34 +++++++++++++++++++ 8 files changed, 69 insertions(+), 31 deletions(-) create mode 100644 packages/react-form/src/AppForm/ReactAppFieldApi.lib.ts diff --git a/packages/form-core/src/FormApi/FormApi.lib.ts b/packages/form-core/src/FormApi/FormApi.lib.ts index c73729e2c..020e75e54 100644 --- a/packages/form-core/src/FormApi/FormApi.lib.ts +++ b/packages/form-core/src/FormApi/FormApi.lib.ts @@ -214,7 +214,9 @@ export class InternalFormApi< static majorVersion = 2 // Allows adapters to control which InternalFieldApi subclass creates fields. - _FieldApi = InternalFieldApi + get _FieldApi(): typeof InternalFieldApi { + return InternalFieldApi + } atom: ReadonlyAtom< FormState> > diff --git a/packages/react-form/src/AppForm/Components.lib.tsx b/packages/react-form/src/AppForm/Components.lib.tsx index c5980984e..2e7f2addd 100644 --- a/packages/react-form/src/AppForm/Components.lib.tsx +++ b/packages/react-form/src/AppForm/Components.lib.tsx @@ -33,11 +33,10 @@ type AnyFieldComponent = FunctionComponent< export function createFieldWithContext( form: InternalReactFormApi, - fieldComponents: Record>, scope: FieldOptionsScope, ) { const TanStackFormField: AnyFieldComponent = (props) => { - const fieldApi = useField({ ...props, form }, fieldComponents, scope) + const fieldApi = useField({ ...props, form }, scope) const field = useValueFieldSubscription(fieldApi) return ( diff --git a/packages/react-form/src/AppForm/ReactAppFieldApi.lib.ts b/packages/react-form/src/AppForm/ReactAppFieldApi.lib.ts new file mode 100644 index 000000000..e95d4c045 --- /dev/null +++ b/packages/react-form/src/AppForm/ReactAppFieldApi.lib.ts @@ -0,0 +1,14 @@ +import { InternalFieldApi } from '@tanstack/form-core/internals' +import type { FunctionComponent } from 'react' + +type FieldComponents = Record> + +export function createInternalReactAppFieldApiClass( + fieldComponents: FieldComponents, +): typeof InternalFieldApi { + class InternalReactAppFieldApi extends InternalFieldApi {} + + Object.assign(InternalReactAppFieldApi.prototype, fieldComponents) + + return InternalReactAppFieldApi as typeof InternalFieldApi +} diff --git a/packages/react-form/src/AppForm/ReactAppFormApi.lib.ts b/packages/react-form/src/AppForm/ReactAppFormApi.lib.ts index ece94c66b..5a9dc98b8 100644 --- a/packages/react-form/src/AppForm/ReactAppFormApi.lib.ts +++ b/packages/react-form/src/AppForm/ReactAppFormApi.lib.ts @@ -1,5 +1,6 @@ import { createArrayFieldComponent } from '../ReactForm/Components.lib' import { InternalReactFormApi } from '../ReactForm/ReactFormApi.lib' +import { createInternalReactAppFieldApiClass } from './ReactAppFieldApi.lib' import { createAppForm, createFieldWithContext, @@ -28,23 +29,29 @@ export function createInternalReactAppFormApiClass< formComponents: TFormComponents, fieldComponents: FormComponents, ): InternalReactAppFormApiConstructor { + const InternalReactAppFieldApi = + createInternalReactAppFieldApiClass(fieldComponents) + class InternalReactAppFormApi extends InternalReactFormApi implements InternalReactAppFormApiInstance { AppForm: AppFormComponent + override get _FieldApi(): typeof InternalReactAppFieldApi { + return InternalReactAppFieldApi + } + constructor( options: FormOptions, defaultOptions?: DefaultOptions, ) { - super(options, defaultOptions, fieldComponents) + super(options, defaultOptions) - const field = createFieldWithContext(this, fieldComponents, 'field') - const groupField = createFieldWithContext(this, fieldComponents, 'field') + const field = createFieldWithContext(this, 'field') + const groupField = createFieldWithContext(this, 'field') const groupArrayField = createArrayFieldComponent( this, - fieldComponents, 'field', ) as FunctionComponent diff --git a/packages/react-form/src/ReactForm/Components.lib.tsx b/packages/react-form/src/ReactForm/Components.lib.tsx index 18d9ae432..cfc599974 100644 --- a/packages/react-form/src/ReactForm/Components.lib.tsx +++ b/packages/react-form/src/ReactForm/Components.lib.tsx @@ -21,11 +21,10 @@ type AnyFieldComponent = FunctionComponent< export function createFieldComponent( form: InternalReactFormApi, - fieldComponents: Record> | null, scope: FieldOptionsScope, ): AnyFieldComponent { const TanStackFormField: AnyFieldComponent = (props) => { - const fieldApi = useField({ ...props, form }, fieldComponents, scope) + const fieldApi = useField({ ...props, form }, scope) const field = useValueFieldSubscription(fieldApi) return props.children(field) @@ -43,11 +42,10 @@ type AnyArrayFieldComponent = { export function createArrayFieldComponent( form: InternalReactFormApi, - fieldComponents: Record> | null, scope: FieldOptionsScope, ): AnyArrayFieldComponent { const TanStackFormArrayField: AnyArrayFieldComponent = (props) => { - const fieldApi = useField({ ...props, form }, fieldComponents, scope) + const fieldApi = useField({ ...props, form }, scope) const field = useArrayFieldSubscription(fieldApi) return props.children(field) @@ -81,7 +79,6 @@ type AnyFormGroupComponent = FunctionComponent< export function createFormGroupComponent( form: InternalReactFormApi, - fieldComponents: Record> | null, ): AnyFormGroupComponent { const TanStackFormGroup: AnyFormGroupComponent = (props) => { const groupRef = @@ -91,7 +88,6 @@ export function createFormGroupComponent( groupRef.current = attachReactFormGroupComponents( new InternalFormGroupApi({ ...props, form } as never), form, - fieldComponents, ) } @@ -114,7 +110,6 @@ export function createFormGroupComponent( function attachReactFormGroupComponents( group: InternalFormGroupApi, form: InternalReactFormApi, - fieldComponents: Record> | null, ) { type FormGroupComponents = InternalFormGroupApi & { Field: FunctionComponent @@ -123,12 +118,8 @@ function attachReactFormGroupComponents( } const resultGroup: FormGroupComponents = group as never - const GroupField = createFieldComponent(form, fieldComponents, 'field') - const GroupArrayField = createArrayFieldComponent( - form, - fieldComponents, - 'field', - ) + const GroupField = createFieldComponent(form, 'field') + const GroupArrayField = createArrayFieldComponent(form, 'field') resultGroup.Field = function Field(props) { return ( diff --git a/packages/react-form/src/ReactForm/ReactFormApi.lib.tsx b/packages/react-form/src/ReactForm/ReactFormApi.lib.tsx index e2bedfcfc..c3e6375c4 100644 --- a/packages/react-form/src/ReactForm/ReactFormApi.lib.tsx +++ b/packages/react-form/src/ReactForm/ReactFormApi.lib.tsx @@ -8,7 +8,6 @@ import { createSubscribeComponent, } from './Components.lib' import type { DefaultOptions, FormOptions } from '@tanstack/form-core' -import type { FunctionComponent } from 'react' import type { ReactTanStackFormComponents } from './Components.public' const useReactId = @@ -26,19 +25,16 @@ export class InternalReactFormApi constructor( options: FormOptions, defaultOptions?: DefaultOptions, - fieldComponents: Record> | null = null, ) { super(options, defaultOptions) this.Field = createFieldComponent( this, - fieldComponents, 'field', ) as InternalReactFormApi['Field'] - this.ArrayField = createArrayFieldComponent(this, fieldComponents, 'field') + this.ArrayField = createArrayFieldComponent(this, 'field') this.Subscribe = createSubscribeComponent(this) this.FormGroup = createFormGroupComponent( this, - fieldComponents, ) as InternalReactFormApi['FormGroup'] } } diff --git a/packages/react-form/src/ReactForm/useField.lib.ts b/packages/react-form/src/ReactForm/useField.lib.ts index 8f66c6873..ddb5dbb0a 100644 --- a/packages/react-form/src/ReactForm/useField.lib.ts +++ b/packages/react-form/src/ReactForm/useField.lib.ts @@ -1,6 +1,5 @@ import { useEffect, useMemo, useRef } from 'react' import { useSelector } from '@tanstack/react-store' -import type { FunctionComponent } from 'react' import type { AnyInternalFieldApi, FieldOptionsScope, @@ -23,7 +22,6 @@ interface InternalFieldProps extends ReactFormFieldProps< export function useField( options: InternalFieldProps, - fieldComponents: Record> | null, scope: FieldOptionsScope, ): AnyInternalFieldApi { const optionsRef = useRef(options) @@ -40,11 +38,8 @@ export function useField( }, scope, ) - if (fieldComponents === null) return field - Object.assign(field, fieldComponents) - return field - }, [options.name, options.form, resetVersion, fieldComponents, scope]) + }, [options.name, options.form, resetVersion, scope]) useEffect(() => fieldApi._update(options, scope)) diff --git a/packages/react-form/tests/createFormHook.spec.tsx b/packages/react-form/tests/createFormHook.spec.tsx index 0edff7402..c1b7a0720 100644 --- a/packages/react-form/tests/createFormHook.spec.tsx +++ b/packages/react-form/tests/createFormHook.spec.tsx @@ -8,6 +8,40 @@ import type { } from '@tanstack/form-core/internals' describe('createFormHook', () => { + it('provides registered components to fields created during mount validation', () => { + const SharedComponent = () => Shared field component + const { useAppForm } = createFormHook({ + fieldComponents: { SharedComponent }, + formComponents: {}, + }) + + function Component() { + const form = useAppForm({ + defaultValues: { name: '' }, + validators: [ + { + runOnMount: true, + triggers: [], + run: () => ({ fields: { name: 'Name is required' } }), + }, + ], + }) + + return ( + + {(field) => { + const { SharedComponent: DestructuredComponent } = field + return + }} + + ) + } + + const { getByText } = render() + + expect(getByText('Shared field component')).toBeInTheDocument() + }) + it('supports destructuring registered form components', () => { const SharedComponent = () => Shared component const { useAppForm } = createFormHook({ From b9d19d873969caffbdef7a8cf53348a0858ce791 Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:26:40 +0200 Subject: [PATCH 5/5] chore: fix docs ref and knip --- docs/config.json | 2 +- packages/react-form/src/AppForm/ReactAppFormApi.lib.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/config.json b/docs/config.json index 57e9a2baf..7fd0732a8 100644 --- a/docs/config.json +++ b/docs/config.json @@ -497,7 +497,7 @@ }, { "label": "FieldGroupHelper", - "to": "framework/react/reference/interfaces/FieldGroupHelper" + "to": "reference/interfaces/FieldGroupHelper" } ] } diff --git a/packages/react-form/src/AppForm/ReactAppFormApi.lib.ts b/packages/react-form/src/AppForm/ReactAppFormApi.lib.ts index 5a9dc98b8..577020f5c 100644 --- a/packages/react-form/src/AppForm/ReactAppFormApi.lib.ts +++ b/packages/react-form/src/AppForm/ReactAppFormApi.lib.ts @@ -12,7 +12,7 @@ import type { AppFormComponent } from './ReactAppFormApi.public' type FormComponents = Record> -export interface InternalReactAppFormApiInstance extends InternalReactFormApi { +interface InternalReactAppFormApiInstance extends InternalReactFormApi { AppForm: AppFormComponent }