diff --git a/examples/react/custom-component-wrapper/.eslintrc.cjs b/examples/react/custom-component-wrapper/.eslintrc.cjs
new file mode 100644
index 000000000..35853b617
--- /dev/null
+++ b/examples/react/custom-component-wrapper/.eslintrc.cjs
@@ -0,0 +1,11 @@
+// @ts-check
+
+/** @type {import('eslint').Linter.Config} */
+const config = {
+ extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
+ rules: {
+ 'react/no-children-prop': 'off',
+ },
+}
+
+module.exports = config
diff --git a/examples/react/custom-component-wrapper/.gitignore b/examples/react/custom-component-wrapper/.gitignore
new file mode 100644
index 000000000..4673b022e
--- /dev/null
+++ b/examples/react/custom-component-wrapper/.gitignore
@@ -0,0 +1,27 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+pnpm-lock.yaml
+yarn.lock
+package-lock.json
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/examples/react/custom-component-wrapper/README.md b/examples/react/custom-component-wrapper/README.md
new file mode 100644
index 000000000..1cf889265
--- /dev/null
+++ b/examples/react/custom-component-wrapper/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `npm install`
+- `npm run dev`
diff --git a/examples/react/custom-component-wrapper/index.html b/examples/react/custom-component-wrapper/index.html
new file mode 100644
index 000000000..5d0e76cd4
--- /dev/null
+++ b/examples/react/custom-component-wrapper/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+ TanStack Form React Simple Example App
+
+
+
+
+
+
+
diff --git a/examples/react/custom-component-wrapper/package.json b/examples/react/custom-component-wrapper/package.json
new file mode 100644
index 000000000..16d4e22ff
--- /dev/null
+++ b/examples/react/custom-component-wrapper/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@tanstack/form-example-react-custom-component-wrapper",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3001",
+ "build": "vite build",
+ "preview": "vite preview",
+ "test:types": "tsc"
+ },
+ "dependencies": {
+ "@tanstack/react-form": "^0.26.1",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "vite": "^5.3.3"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/examples/react/custom-component-wrapper/public/emblem-light.svg b/examples/react/custom-component-wrapper/public/emblem-light.svg
new file mode 100644
index 000000000..a58e69ad5
--- /dev/null
+++ b/examples/react/custom-component-wrapper/public/emblem-light.svg
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/examples/react/custom-component-wrapper/src/index.tsx b/examples/react/custom-component-wrapper/src/index.tsx
new file mode 100644
index 000000000..7bc91d2e4
--- /dev/null
+++ b/examples/react/custom-component-wrapper/src/index.tsx
@@ -0,0 +1,132 @@
+import * as React from 'react'
+import { createRoot } from 'react-dom/client'
+import { useForm } from '@tanstack/react-form'
+import type {
+ DeepKeyValueName,
+ FieldOptions,
+ ReactFormApi,
+} from '@tanstack/react-form'
+
+/**
+ * Export these components to your design system or a dedicated component location
+ */
+interface TextInputFieldProps<
+ TFormData extends unknown,
+ TName extends DeepKeyValueName,
+> extends FieldOptions {
+ form: ReactFormApi
+ // Your custom props
+ label: string
+}
+
+function TextInputField<
+ TFormData extends unknown,
+ TName extends DeepKeyValueName,
+>({ form, name, label, ...fieldProps }: TextInputFieldProps) {
+ return (
+ // Manually type-cast form.Field to work around this issue:
+ // https://twitter.com/crutchcorn/status/1809827621485900049
+
+ {...fieldProps}
+ name={name}
+ children={(field) => {
+ return (
+
+
+
+
+
field.handleChange(e.target.value)}
+ />
+ {field.state.meta.isValidating ? (
+
Validating...
+ ) : null}
+ {field.state.meta.isTouched && field.state.meta.errors.length ? (
+
+ {field.state.meta.errors.join(', ')}
+
+ ) : null}
+
+ )
+ }}
+ />
+ )
+}
+
+function SubmitButton({ form }: { form: ReactFormApi }) {
+ return (
+ [state.canSubmit, state.isSubmitting]}
+ children={([canSubmit, isSubmitting]) => (
+ <>
+
+ >
+ )}
+ />
+ )
+}
+
+/**
+ * Then use it in your application
+ */
+export default function App() {
+ const form = useForm({
+ defaultValues: {
+ firstName: '',
+ age: 0,
+ },
+ onSubmit: async ({ value }) => {
+ // Do something with form data
+ console.log(value)
+ },
+ })
+
+ return (
+
+
Wrapped Fields Form Example
+
+
+ )
+}
+
+const rootElement = document.getElementById('root')!
+
+createRoot(rootElement).render(
+
+
+ ,
+)
diff --git a/examples/react/custom-component-wrapper/tsconfig.json b/examples/react/custom-component-wrapper/tsconfig.json
new file mode 100644
index 000000000..666a0ea71
--- /dev/null
+++ b/examples/react/custom-component-wrapper/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "jsx": "react",
+ "noEmit": true,
+ "strict": true,
+ "esModuleInterop": true,
+ "lib": ["DOM", "DOM.Iterable", "ES2020"]
+ }
+}
diff --git a/packages/form-core/src/util-types.ts b/packages/form-core/src/util-types.ts
index 8a9a107cd..e00642173 100644
--- a/packages/form-core/src/util-types.ts
+++ b/packages/form-core/src/util-types.ts
@@ -111,7 +111,6 @@ export type DeepValue<
TValue,
// A string representing the path of the property we're trying to access
TAccessor,
- TNullable extends boolean = IsNullable,
> =
// If TValue is any it will recurse forever, this terminates the recursion
unknown extends TValue
@@ -138,9 +137,21 @@ export type DeepValue<
: TAccessor extends `${infer TBefore}.${infer TAfter}`
? DeepValue, TAfter>
: TAccessor extends string
- ? TNullable extends true
+ ? IsNullable extends true
? Nullable
: TValue[TAccessor]
: never
: // Do not allow `TValue` to be anything else
never
+
+type SelfKeys = {
+ [K in keyof T]: K
+}[keyof T]
+
+// Utility type to narrow allowed TName values to only specific types
+// IE: DeepKeyValueName<{ foo: string; bar: number }, string> = 'foo'
+export type DeepKeyValueName = SelfKeys<{
+ [K in DeepKeys as DeepValue extends TField
+ ? K
+ : never]: K
+}>
diff --git a/packages/form-core/tests/util-types.test-d.ts b/packages/form-core/tests/util-types.test-d.ts
index cf692c845..8870709f5 100644
--- a/packages/form-core/tests/util-types.test-d.ts
+++ b/packages/form-core/tests/util-types.test-d.ts
@@ -1,5 +1,5 @@
import { assertType } from 'vitest'
-import type { DeepKeys, DeepValue } from '../src/index'
+import { DeepKeys, DeepKeyValueName, DeepValue } from '../src/index'
/**
* Properly recognizes that `0` is not an object and should not have subkeys
@@ -169,3 +169,30 @@ type DoubleDeepArray = DeepValue<
>
assertType(0 as never as DoubleDeepArray)
+
+type FooBarOther = {
+ foo: string
+ bar: string
+ other: number
+}
+
+type StringFromFooBar = DeepKeyValueName
+
+assertType<'foo' | 'bar'>(0 as never as StringFromFooBar)
+
+type DeepFooBarOther = {
+ foo: string
+ bar: string
+ other: number
+ one: {
+ foo: string
+ bar: string
+ other: number
+ }
+}
+
+type StringFromDeepFooBar = DeepKeyValueName
+
+assertType<'foo' | 'bar' | 'one.foo' | 'one.bar'>(
+ 0 as never as StringFromDeepFooBar,
+)
diff --git a/packages/react-form/src/index.ts b/packages/react-form/src/index.ts
index e35e2c602..80234f658 100644
--- a/packages/react-form/src/index.ts
+++ b/packages/react-form/src/index.ts
@@ -1,6 +1,6 @@
export * from '@tanstack/form-core'
-export { useForm } from './useForm'
+export { useForm, type ReactFormApi } from './useForm'
export type { UseField, FieldComponent } from './useField'
export { useField, Field } from './useField'
diff --git a/packages/react-form/src/useField.tsx b/packages/react-form/src/useField.tsx
index 9bb8c77a1..25401f86e 100644
--- a/packages/react-form/src/useField.tsx
+++ b/packages/react-form/src/useField.tsx
@@ -5,18 +5,6 @@ import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
import type { NodeType, UseFieldOptions } from './types'
import type { DeepKeys, DeepValue, Validator } from '@tanstack/form-core'
-interface ReactFieldApi<
- TParentData,
- TFormValidator extends
- | Validator
- | undefined = undefined,
-> {
- /**
- * A pre-bound and type-safe sub-field component using this field as a root.
- */
- Field: FieldComponent
-}
-
/**
* A type representing a hook for using a field in a form with the given form data type.
*
@@ -66,18 +54,11 @@ export function useField<
>,
) {
const [fieldApi] = useState(() => {
- const api = new FieldApi({
+ return new FieldApi({
...opts,
form: opts.form,
name: opts.name,
})
-
- const extendedApi: typeof api & ReactFieldApi =
- api as never
-
- extendedApi.Field = Field as never
-
- return extendedApi
})
useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi])
diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx
index 173a505ad..cab1f19cc 100644
--- a/packages/react-form/src/useForm.tsx
+++ b/packages/react-form/src/useForm.tsx
@@ -10,7 +10,7 @@ import type { NodeType } from './types'
/**
* Fields that are added onto the `FormAPI` from `@tanstack/form-core` and returned from `useForm`
*/
-interface ReactFormApi<
+export interface ReactFormApi<
TFormData,
TFormValidator extends Validator | undefined = undefined,
> {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e3445fdcd..8a2b80ebb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -437,6 +437,31 @@ importers:
specifier: ^5.3.3
version: 5.3.3(@types/node@20.14.10)(less@4.2.0)(sass@1.72.0)(sugarss@4.0.1(postcss@8.4.39))(terser@5.29.1)
+ examples/react/custom-component-wrapper:
+ dependencies:
+ '@tanstack/react-form':
+ specifier: ^0.26.1
+ version: link:../../../packages/react-form
+ react:
+ specifier: ^18.3.1
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.3.1
+ version: 18.3.1(react@18.3.1)
+ devDependencies:
+ '@types/react':
+ specifier: ^18.3.3
+ version: 18.3.3
+ '@types/react-dom':
+ specifier: ^18.3.0
+ version: 18.3.0
+ '@vitejs/plugin-react':
+ specifier: ^4.3.1
+ version: 4.3.1(vite@5.3.3(@types/node@20.10.6)(less@4.2.0)(sass@1.72.0)(sugarss@4.0.1(postcss@8.4.39))(terser@5.29.1))
+ vite:
+ specifier: ^5.3.3
+ version: 5.3.3(@types/node@20.10.6)(less@4.2.0)(sass@1.72.0)(sugarss@4.0.1(postcss@8.4.39))(terser@5.29.1)
+
examples/react/next-server-actions:
dependencies:
'@tanstack/react-form':