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
2 changes: 2 additions & 0 deletions packages/react-form-devtools/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export default [
'react-hooks/exhaustive-deps': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-compiler/react-compiler': 'error',
// Must be "off" to avoid moving `useContext` to `use`, which breaks React 17/18 usage.
'@eslint-react/no-use-context': 'off',
},
},
]
2 changes: 2 additions & 0 deletions packages/react-form-nextjs/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export default [
'react-hooks/exhaustive-deps': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-compiler/react-compiler': 'error',
// Must be "off" to avoid moving `useContext` to `use`, which breaks React 17/18 usage.
'@eslint-react/no-use-context': 'off',
},
},
]
2 changes: 2 additions & 0 deletions packages/react-form-remix/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export default [
'react-hooks/exhaustive-deps': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-compiler/react-compiler': 'error',
// Must be "off" to avoid moving `useContext` to `use`, which breaks React 17/18 usage.
'@eslint-react/no-use-context': 'off',
},
},
]
2 changes: 2 additions & 0 deletions packages/react-form-start/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export default [
'react-hooks/exhaustive-deps': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-compiler/react-compiler': 'error',
// Must be "off" to avoid moving `useContext` to `use`, which breaks React 17/18 usage.
'@eslint-react/no-use-context': 'off',
},
},
]
2 changes: 2 additions & 0 deletions packages/react-form/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export default [
'react-hooks/exhaustive-deps': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-compiler/react-compiler': 'error',
// Must be "off" to avoid moving `useContext` to `use`, which breaks React 17/18 usage.
'@eslint-react/no-use-context': 'off',
},
},
]
1 change: 0 additions & 1 deletion packages/react-form/src/createFormHook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,6 @@ export function createFormHook<
return (
<form.Field {...props}>
{(field) => (
// eslint-disable-next-line @eslint-react/no-context-provider
<fieldContext.Provider value={field}>
{children(Object.assign(field, fieldComponents))}
</fieldContext.Provider>
Expand Down
55 changes: 3 additions & 52 deletions packages/react-form/src/useField.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useMemo, useRef, useState } from 'react'
import { useMemo, useState } from 'react'
import { useStore } from '@tanstack/react-store'
import { FieldApi, functionalUpdate } from '@tanstack/form-core'
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
Expand All @@ -15,42 +15,9 @@ import type {
FormAsyncValidateOrFn,
FormValidateOrFn,
} from '@tanstack/form-core'
import type { FunctionComponent, ReactElement, ReactNode } from 'react'
import type { FunctionComponent, ReactNode } from 'react'
import type { UseFieldOptions, UseFieldOptionsBound } from './types'

interface ReactFieldApi<
TParentData,
TFormOnMount extends undefined | FormValidateOrFn<TParentData>,
TFormOnChange extends undefined | FormValidateOrFn<TParentData>,
TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
TFormOnBlur extends undefined | FormValidateOrFn<TParentData>,
TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>,
TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
TFormOnDynamic extends undefined | FormValidateOrFn<TParentData>,
TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>,
TPatentSubmitMeta,
> {
/**
* A pre-bound and type-safe sub-field component using this field as a root.
*/
Field: FieldComponent<
TParentData,
TFormOnMount,
TFormOnChange,
TFormOnChangeAsync,
TFormOnBlur,
TFormOnBlurAsync,
TFormOnSubmit,
TFormOnSubmitAsync,
TFormOnDynamic,
TFormOnDynamicAsync,
TFormOnServer,
TPatentSubmitMeta
>
}

/**
* A type representing a hook for using a field in a form with the given form data type.
*
Expand Down Expand Up @@ -305,23 +272,7 @@ export function useField<
TFormOnDynamicAsync,
TFormOnServer,
TPatentSubmitMeta
> &
ReactFieldApi<
TParentData,
TFormOnMount,
TFormOnChange,
TFormOnChangeAsync,
TFormOnBlur,
TFormOnBlurAsync,
TFormOnSubmit,
TFormOnSubmitAsync,
TFormOnDynamic,
TFormOnDynamicAsync,
TFormOnServer,
TPatentSubmitMeta
> = reactiveFieldApi as never

extendedApi.Field = Field as never
> = reactiveFieldApi as never
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return extendedApi
}, [
Expand Down
107 changes: 1 addition & 106 deletions packages/solid-form/src/createField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import type {
FieldValidators,
FormAsyncValidateOrFn,
FormValidateOrFn,
Narrow,
} from '@tanstack/form-core'

import type { Accessor, JSX, JSXElement } from 'solid-js'
Expand Down Expand Up @@ -51,94 +50,6 @@ interface SolidFieldApi<
>
}

export type CreateField<
TParentData,
TFormOnMount extends undefined | FormValidateOrFn<TParentData>,
TFormOnChange extends undefined | FormValidateOrFn<TParentData>,
TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
TFormOnBlur extends undefined | FormValidateOrFn<TParentData>,
TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>,
TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
TFormOnDynamic extends undefined | FormValidateOrFn<TParentData>,
TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>,
TParentSubmitMeta,
> = <
TName extends DeepKeys<TParentData>,
TData extends DeepValue<TParentData, TName>,
TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
TOnChangeAsync extends
| undefined
| FieldAsyncValidateOrFn<TParentData, TName, TData>,
TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
TOnBlurAsync extends
| undefined
| FieldAsyncValidateOrFn<TParentData, TName, TData>,
TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
TOnSubmitAsync extends
| undefined
| FieldAsyncValidateOrFn<TParentData, TName, TData>,
TOnDynamic extends undefined | FieldValidateOrFn<TParentData, TName, TData>,
TOnDynamicAsync extends
| undefined
| FieldAsyncValidateOrFn<TParentData, TName, TData>,
>(
opts: () => { name: Narrow<TName> } & CreateFieldOptionsBound<
TParentData,
TName,
TData,
TOnMount,
TOnChange,
TOnChangeAsync,
TOnBlur,
TOnBlurAsync,
TOnSubmit,
TOnSubmitAsync,
TOnDynamic,
TOnDynamicAsync
>,
) => () => FieldApi<
TParentData,
TName,
TData,
TOnMount,
TOnChange,
TOnChangeAsync,
TOnBlur,
TOnBlurAsync,
TOnSubmit,
TOnSubmitAsync,
TOnDynamic,
TOnDynamicAsync,
TFormOnMount,
TFormOnChange,
TFormOnChangeAsync,
TFormOnBlur,
TFormOnBlurAsync,
TFormOnSubmit,
TFormOnSubmitAsync,
TFormOnDynamic,
TFormOnDynamicAsync,
TFormOnServer,
TParentSubmitMeta
> &
SolidFieldApi<
TParentData,
TFormOnMount,
TFormOnChange,
TFormOnChangeAsync,
TFormOnBlur,
TFormOnBlurAsync,
TFormOnSubmit,
TFormOnSubmitAsync,
TFormOnDynamic,
TFormOnDynamicAsync,
TFormOnServer,
TParentSubmitMeta
>

// ugly way to trick solid into triggering updates for changes on the fieldApi
function makeFieldReactive<
TParentData,
Expand Down Expand Up @@ -326,23 +237,7 @@ export function createField<

const api = new FieldApi(options)

const extendedApi: typeof api &
SolidFieldApi<
TParentData,
TFormOnMount,
TFormOnChange,
TFormOnChangeAsync,
TFormOnBlur,
TFormOnBlurAsync,
TFormOnSubmit,
TFormOnSubmitAsync,
TFormOnDynamic,
TFormOnDynamicAsync,
TFormOnServer,
TParentSubmitMeta
> = api as never

extendedApi.Field = Field as never
const extendedApi: typeof api = api as never
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify mismatch: runtime assignment removed, but type still includes SolidFieldApi in createField path.
rg -n -C3 "const extendedApi: typeof api = api as never|extendedApi\\.Field\\s*=|function makeFieldReactive|SolidFieldApi<" packages/solid-form/src/createField.tsx

Repository: TanStack/form

Length of output: 1119


🏁 Script executed:

# First, let's see the full SolidFieldApi interface definition
sed -n '23,52p' packages/solid-form/src/createField.tsx

Repository: TanStack/form

Length of output: 1112


🏁 Script executed:

# Now let's see the full context around line 240 and how extendedApi is used after
sed -n '235,280p' packages/solid-form/src/createField.tsx

Repository: TanStack/form

Length of output: 1033


🏁 Script executed:

# Check if extendedApi.Field is assigned anywhere in the file
rg -n "extendedApi\.Field\s*=" packages/solid-form/src/createField.tsx

Repository: TanStack/form

Length of output: 39


🏁 Script executed:

# Also check the return type and return statement of makeFieldReactive
sed -n '150,160p' packages/solid-form/src/createField.tsx

Repository: TanStack/form

Length of output: 258


🏁 Script executed:

# Get the complete return statement starting from line 250+
sed -n '260,300p' packages/solid-form/src/createField.tsx

Repository: TanStack/form

Length of output: 1054


🏁 Script executed:

# Check all uses of extendedApi in the file
rg -n "extendedApi" packages/solid-form/src/createField.tsx

Repository: TanStack/form

Length of output: 138


🏁 Script executed:

# Also, let's see the makeFieldReactive function's return statement
sed -n '155,180p' packages/solid-form/src/createField.tsx

Repository: TanStack/form

Length of output: 774


🏁 Script executed:

# Get the createField function signature with return type
sed -n '167,210p' packages/solid-form/src/createField.tsx

Repository: TanStack/form

Length of output: 1952


🏁 Script executed:

# Get the return type annotation of createField
sed -n '207,225p' packages/solid-form/src/createField.tsx

Repository: TanStack/form

Length of output: 439


🏁 Script executed:

# Get more of the function signature to find the return type
sed -n '220,240p' packages/solid-form/src/createField.tsx

Repository: TanStack/form

Length of output: 462


🏁 Script executed:

# Search for the return type annotation more precisely
rg -n "^\) " packages/solid-form/src/createField.tsx | head -5

Repository: TanStack/form

Length of output: 73


🏁 Script executed:

# Also check the full createField signature with better context
sed -n '174,236p' packages/solid-form/src/createField.tsx | grep -n "=>"

Repository: TanStack/form

Length of output: 94


🏁 Script executed:

# Get a cleaner view of the full createField function signature
awk '/^export function createField</,/^\) \{/' packages/solid-form/src/createField.tsx | tail -30

Repository: TanStack/form

Length of output: 694


🏁 Script executed:

# Check if Field property is ever added anywhere in the createField function
sed -n '238,290p' packages/solid-form/src/createField.tsx

Repository: TanStack/form

Length of output: 1183


Type contract advertises field().Field, but runtime never provides it.

createField passes extendedApi to makeFieldReactive without adding a Field property, yet the return type signature includes SolidFieldApi, which declares Field: FieldComponent<...>. This creates a compile-time-success / runtime-undefined trap: consumers can write field().Field and TypeScript accepts it, but the property is never defined at runtime.

The root cause is that extendedApi on line 240 is never enriched with the Field property before being passed to makeFieldReactive on line 288. Meanwhile, makeFieldReactive's return type contract (() => FieldApi<...> & SolidFieldApi<...>) still advertises Field via SolidFieldApi.

Either add the Field property to extendedApi before passing it to makeFieldReactive, or remove SolidFieldApi from the return type to match the actual runtime behavior.

🤖 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/solid-form/src/createField.tsx` at line 240, The return type
advertises SolidFieldApi.Field but extendedApi (created in createField and
passed into makeFieldReactive) never gets a Field property, so consumers see
field().Field at compile-time but it's undefined at runtime; fix by adding a
proper Field implementation onto extendedApi before passing it to
makeFieldReactive (implement a FieldComponent matching SolidFieldApi's Field
signature and assign extendedApi.Field = <that component>), or alternatively
remove SolidFieldApi from the createField/makeFieldReactive return type so the
type matches runtime — update the symbols extendedApi, createField,
makeFieldReactive, and SolidFieldApi accordingly to keep type and runtime
consistent.


let mounted = false
// Instantiates field meta and removes it when unrendered
Expand Down
20 changes: 1 addition & 19 deletions packages/solid-form/src/createForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
FormValidateOrFn,
} from '@tanstack/form-core'
import type { JSXElement } from 'solid-js'
import type { CreateField, FieldComponent } from './createField'
import type { FieldComponent } from './createField'

export interface SolidFormApi<
TParentData,
Expand Down Expand Up @@ -39,20 +39,6 @@ export interface SolidFormApi<
TFormOnServer,
TSubmitMeta
>
createField: CreateField<
TParentData,
TFormOnMount,
TFormOnChange,
TFormOnChangeAsync,
TFormOnBlur,
TFormOnBlurAsync,
TFormOnSubmit,
TFormOnSubmitAsync,
TFormOnDynamic,
TFormOnDynamicAsync,
TFormOnServer,
TSubmitMeta
>
useStore: <
TSelected = NoInfer<
FormState<
Expand Down Expand Up @@ -231,10 +217,6 @@ export function createForm<
> = api as never

extendedApi.Field = (props) => <Field {...props} form={api} />
extendedApi.createField = (props) =>
createField(() => {
return { ...props(), form: api }
}) as never
extendedApi.useStore = (selector) => useStore(api.store, selector)
extendedApi.Subscribe = (props) =>
functionalUpdate(props.children, useStore(api.store, props.selector))
Expand Down
21 changes: 2 additions & 19 deletions packages/svelte-form/src/Field.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
} from '@tanstack/form-core'
import { useStore } from '@tanstack/svelte-store'
import { onMount, type Snippet } from 'svelte'
import Field from './Field.svelte'
import type { CreateFieldOptions, SvelteFieldApi } from './types.js'
import type { CreateFieldOptions } from './types.js'

export function createField<
TParentData,
Expand Down Expand Up @@ -76,23 +75,7 @@

const api = new FieldApi(options)

const extendedApi: typeof api &
SvelteFieldApi<
TParentData,
TFormOnMount,
TFormOnChange,
TFormOnChangeAsync,
TFormOnBlur,
TFormOnBlurAsync,
TFormOnSubmit,
TFormOnSubmitAsync,
TFormOnDynamic,
TFormOnDynamicAsync,
TFormOnServer,
TParentSubmitMeta
> = api as never

extendedApi.Field = Field as never
const extendedApi: typeof api = api as never

let mounted = false
// Instantiates field meta and removes it when unrendered
Expand Down
22 changes: 2 additions & 20 deletions packages/svelte-form/src/createForm.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FormApi } from '@tanstack/form-core'
import { useStore } from '@tanstack/svelte-store'
import { onMount } from 'svelte'
import Field, { createField } from './Field.svelte'
import Field from './Field.svelte'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
import Subscribe from './Subscribe.svelte'
import type {
Component,
Expand All @@ -15,7 +15,7 @@ import type {
FormState,
FormValidateOrFn,
} from '@tanstack/form-core'
import type { CreateField, FieldComponent, WithoutFunction } from './types.js'
import type { FieldComponent, WithoutFunction } from './types.js'

export interface SvelteFormApi<
TParentData,
Expand Down Expand Up @@ -45,20 +45,6 @@ export interface SvelteFormApi<
TFormOnServer,
TSubmitMeta
>
createField: CreateField<
TParentData,
TFormOnMount,
TFormOnChange,
TFormOnChangeAsync,
TFormOnBlur,
TFormOnBlurAsync,
TFormOnSubmit,
TFormOnSubmitAsync,
TFormOnDynamic,
TFormOnDynamicAsync,
TFormOnServer,
TSubmitMeta
>
useStore: <
TSelected = NoInfer<
FormState<
Expand Down Expand Up @@ -287,10 +273,6 @@ export function createForm<
// @ts-expect-error constructor definition exists only on a type level
extendedApi.Field = (internal, props) =>
Field(internal, { ...props, form: api as never } as never)
extendedApi.createField = (props) =>
createField(() => {
return { ...props(), form: api } as never
}) as never // Type cast because else "Error: Type instantiation is excessively deep and possibly infinite."
extendedApi.useStore = (selector) => useStore(api.store, selector)
// @ts-expect-error constructor definition exists only on a type level
extendedApi.Subscribe = (internal, props) =>
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte-form/src/createFormCreator.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
} from '@tanstack/form-core'
import type { FieldComponent } from './types.js'
import type { SvelteFormExtendedApi } from './createForm.svelte'
import type { Component, Snippet, SvelteComponent } from 'svelte'
import type { Component, Snippet } from 'svelte'

/**
* TypeScript inferencing is weird.
Expand Down
Loading
Loading