diff --git a/examples/vue/field-components/.gitignore b/examples/vue/field-components/.gitignore
new file mode 100644
index 000000000..449e8098b
--- /dev/null
+++ b/examples/vue/field-components/.gitignore
@@ -0,0 +1,9 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+
+package-lock.json
+yarn.lock
+pnpm-lock.yaml
diff --git a/examples/vue/field-components/README.md b/examples/vue/field-components/README.md
new file mode 100644
index 000000000..28462a4ad
--- /dev/null
+++ b/examples/vue/field-components/README.md
@@ -0,0 +1,6 @@
+# Basic example
+
+To run this example:
+
+- `npm install` or `yarn` or `pnpm i`
+- `npm run dev` or `yarn dev` or `pnpm dev`
diff --git a/examples/vue/field-components/index.html b/examples/vue/field-components/index.html
new file mode 100644
index 000000000..1a850e19e
--- /dev/null
+++ b/examples/vue/field-components/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ TanStack Form Vue Simple Example App
+
+
+
+
+
+
diff --git a/examples/vue/field-components/package.json b/examples/vue/field-components/package.json
new file mode 100644
index 000000000..b15093340
--- /dev/null
+++ b/examples/vue/field-components/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@tanstack/form-example-vue-field-components",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "build:dev": "vite build -m development",
+ "test:types": "vue-tsc",
+ "serve": "vite preview"
+ },
+ "dependencies": {
+ "@tanstack/vue-form": "^1.6.3",
+ "vue": "^3.5.13"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.2.3",
+ "typescript": "5.8.2",
+ "vite": "^6.3.2",
+ "vue-tsc": "^2.2.2"
+ }
+}
diff --git a/examples/vue/field-components/src/App.vue b/examples/vue/field-components/src/App.vue
new file mode 100644
index 000000000..876f29dfe
--- /dev/null
+++ b/examples/vue/field-components/src/App.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+ {{ JSON.stringify(field.TextField, null, 2) }}
+
+
+ Emergency Contact
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/vue/field-components/src/components/SubscribeButton.vue b/examples/vue/field-components/src/components/SubscribeButton.vue
new file mode 100644
index 000000000..910dbbeae
--- /dev/null
+++ b/examples/vue/field-components/src/components/SubscribeButton.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/vue/field-components/src/components/TextField.vue b/examples/vue/field-components/src/components/TextField.vue
new file mode 100644
index 000000000..2f4469b7c
--- /dev/null
+++ b/examples/vue/field-components/src/components/TextField.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ {{ error }}
+
+
+
diff --git a/examples/vue/field-components/src/compositions/form-providers.ts b/examples/vue/field-components/src/compositions/form-providers.ts
new file mode 100644
index 000000000..e748c35bc
--- /dev/null
+++ b/examples/vue/field-components/src/compositions/form-providers.ts
@@ -0,0 +1,4 @@
+import { createFormCompositionContexts } from '@tanstack/vue-form'
+
+export const { fieldProviderKey, injectField, formProviderKey, injectForm } =
+ createFormCompositionContexts()
diff --git a/examples/vue/field-components/src/compositions/form.ts b/examples/vue/field-components/src/compositions/form.ts
new file mode 100644
index 000000000..5ffc914bf
--- /dev/null
+++ b/examples/vue/field-components/src/compositions/form.ts
@@ -0,0 +1,15 @@
+import { createFormComposition } from '@tanstack/vue-form'
+import SubscribeButton from '../components/SubscribeButton.vue'
+import TextField from '../components/TextField.vue'
+import { fieldProviderKey, formProviderKey } from './form-providers.ts'
+
+export const { useAppForm } = createFormComposition({
+ fieldComponents: {
+ TextField,
+ },
+ formComponents: {
+ SubscribeButton,
+ },
+ fieldProviderKey,
+ formProviderKey,
+})
diff --git a/examples/vue/field-components/src/main.ts b/examples/vue/field-components/src/main.ts
new file mode 100644
index 000000000..912d54f8d
--- /dev/null
+++ b/examples/vue/field-components/src/main.ts
@@ -0,0 +1,5 @@
+import { createApp } from 'vue'
+
+import App from './App.vue'
+
+createApp(App).mount('#app')
diff --git a/examples/vue/field-components/src/shims-vue.d.ts b/examples/vue/field-components/src/shims-vue.d.ts
new file mode 100644
index 000000000..ac1ded792
--- /dev/null
+++ b/examples/vue/field-components/src/shims-vue.d.ts
@@ -0,0 +1,5 @@
+declare module '*.vue' {
+ import { DefineComponent } from 'vue'
+ const component: DefineComponent<{}, {}, any>
+ export default component
+}
diff --git a/examples/vue/field-components/src/types.d.ts b/examples/vue/field-components/src/types.d.ts
new file mode 100644
index 000000000..4851e8102
--- /dev/null
+++ b/examples/vue/field-components/src/types.d.ts
@@ -0,0 +1,6 @@
+export interface Post {
+ userId: number
+ id: number
+ title: string
+ body: string
+}
diff --git a/examples/vue/field-components/tsconfig.json b/examples/vue/field-components/tsconfig.json
new file mode 100644
index 000000000..62eb2b161
--- /dev/null
+++ b/examples/vue/field-components/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "preserve",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
+}
diff --git a/examples/vue/field-components/vite.config.ts b/examples/vue/field-components/vite.config.ts
new file mode 100644
index 000000000..804a28720
--- /dev/null
+++ b/examples/vue/field-components/vite.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [vue()],
+ optimizeDeps: {
+ exclude: ['@tanstack/vue-form'],
+ },
+})
diff --git a/packages/vue-form/src/createFormComposition.tsx b/packages/vue-form/src/createFormComposition.tsx
new file mode 100644
index 000000000..fd0981543
--- /dev/null
+++ b/packages/vue-form/src/createFormComposition.tsx
@@ -0,0 +1,281 @@
+import { defineComponent, h, inject, provide } from 'vue'
+import { useForm } from './useForm'
+import type { Component, InjectionKey } from 'vue'
+import type {
+ AnyFieldApi,
+ AnyFormApi,
+ FieldApi,
+ FormAsyncValidateOrFn,
+ FormOptions,
+ FormValidateOrFn,
+} from '@tanstack/form-core'
+import type { FieldComponent } from './useField'
+import type { VueFormExtendedApi } from './useForm'
+
+export function createFormCompositionContexts() {
+ const fieldProviderKey = Symbol() as InjectionKey
+
+ function injectField() {
+ const field = inject(fieldProviderKey)
+
+ if (!field) {
+ throw new Error(
+ '`injectField` only works when within a `fieldComponent` passed to `createFormComposition`',
+ )
+ }
+
+ return field as FieldApi<
+ any,
+ string,
+ TData,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any
+ >
+ }
+
+ const formProviderKey = Symbol() as InjectionKey
+
+ function injectForm() {
+ const form = inject(formProviderKey)
+
+ if (!form) {
+ throw new Error(
+ '`injectForm` only works when within a `formComponent` passed to `createFormHook`',
+ )
+ }
+
+ return form as VueFormExtendedApi<
+ // If you need access to the form data, you need to use `withForm` instead
+ Record,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any,
+ any
+ >
+ }
+
+ return { fieldProviderKey, injectField, formProviderKey, injectForm }
+}
+
+interface CreateFormCompositionProps<
+ TFieldComponents extends Record,
+ TFormComponents extends Record,
+> {
+ fieldComponents: TFieldComponents
+ fieldProviderKey: InjectionKey
+ formComponents: TFormComponents
+ formProviderKey: InjectionKey
+}
+
+type AppFieldExtendedReactFormApi<
+ TFormData,
+ TOnMount extends undefined | FormValidateOrFn,
+ TOnChange extends undefined | FormValidateOrFn,
+ TOnChangeAsync extends undefined | FormAsyncValidateOrFn,
+ TOnBlur extends undefined | FormValidateOrFn,
+ TOnBlurAsync extends undefined | FormAsyncValidateOrFn,
+ TOnSubmit extends undefined | FormValidateOrFn,
+ TOnSubmitAsync extends undefined | FormAsyncValidateOrFn,
+ TOnServer extends undefined | FormAsyncValidateOrFn,
+ TSubmitMeta,
+ TFieldComponents extends Record,
+ TFormComponents extends Record,
+> = VueFormExtendedApi<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnServer,
+ TSubmitMeta
+> &
+ NoInfer & {
+ AppField: FieldComponent<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnServer,
+ TSubmitMeta,
+ NoInfer
+ >
+ AppForm: Component
+ }
+
+export interface WithFormProps<
+ TFormData,
+ TOnMount extends undefined | FormValidateOrFn,
+ TOnChange extends undefined | FormValidateOrFn,
+ TOnChangeAsync extends undefined | FormAsyncValidateOrFn,
+ TOnBlur extends undefined | FormValidateOrFn,
+ TOnBlurAsync extends undefined | FormAsyncValidateOrFn,
+ TOnSubmit extends undefined | FormValidateOrFn,
+ TOnSubmitAsync extends undefined | FormAsyncValidateOrFn,
+ TOnServer extends undefined | FormAsyncValidateOrFn,
+ TSubmitMeta,
+ TFieldComponents extends Record,
+ TFormComponents extends Record,
+ TRenderProps extends Record = Record,
+> extends FormOptions<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnServer,
+ TSubmitMeta
+ > {
+ // Optional, but adds props to the `render` function outside of `form`
+ props?: TRenderProps
+ render: (
+ props: NoInfer & {
+ form: AppFieldExtendedReactFormApi<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnServer,
+ TSubmitMeta,
+ TFieldComponents,
+ TFormComponents
+ >
+ },
+ ) => JSX.Element
+}
+
+export function createFormComposition<
+ const TComponents extends Record,
+ const TFormComponents extends Record,
+>({
+ fieldComponents,
+ fieldProviderKey,
+ formProviderKey,
+ formComponents,
+}: CreateFormCompositionProps) {
+ function useAppForm<
+ TFormData,
+ TOnMount extends undefined | FormValidateOrFn,
+ TOnChange extends undefined | FormValidateOrFn,
+ TOnChangeAsync extends undefined | FormAsyncValidateOrFn,
+ TOnBlur extends undefined | FormValidateOrFn,
+ TOnBlurAsync extends undefined | FormAsyncValidateOrFn,
+ TOnSubmit extends undefined | FormValidateOrFn,
+ TOnSubmitAsync extends undefined | FormAsyncValidateOrFn,
+ TOnServer extends undefined | FormAsyncValidateOrFn,
+ TSubmitMeta,
+ >(
+ props: FormOptions<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnServer,
+ TSubmitMeta
+ >,
+ ): AppFieldExtendedReactFormApi<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnServer,
+ TSubmitMeta,
+ TComponents,
+ TFormComponents
+ > {
+ const form = useForm(props)
+
+ const AppForm = defineComponent((_, { slots }) => {
+ provide(formProviderKey, form)
+ return () => {
+ return slots.default!()
+ }
+ })
+
+ const AppField = defineComponent((props, { slots }) => {
+ return () => {
+ return (
+
+ {({ field }: { field: AnyFieldApi }) =>
+ h({
+ setup: (_) => {
+ provide(fieldProviderKey, field)
+ },
+ render: () => {
+ return slots.default({
+ field: Object.assign(field, fieldComponents),
+ state: field.state,
+ })
+ },
+ })
+ }
+
+ )
+ }
+ }) as FieldComponent<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnServer,
+ TSubmitMeta,
+ TComponents
+ >
+
+ const extendedForm = Object.assign(form, {
+ AppField,
+ AppForm,
+ ...formComponents,
+ })
+
+ return extendedForm
+ }
+
+ return {
+ useAppForm,
+ }
+}
diff --git a/packages/vue-form/src/index.ts b/packages/vue-form/src/index.ts
index 50f1cbaad..a984f05c3 100644
--- a/packages/vue-form/src/index.ts
+++ b/packages/vue-form/src/index.ts
@@ -2,3 +2,4 @@ export * from '@tanstack/form-core'
export { useStore } from '@tanstack/vue-store'
export * from './useField'
export * from './useForm'
+export * from './createFormComposition'
diff --git a/packages/vue-form/src/useField.tsx b/packages/vue-form/src/useField.tsx
index c63631431..e6a872f4b 100644
--- a/packages/vue-form/src/useField.tsx
+++ b/packages/vue-form/src/useField.tsx
@@ -32,6 +32,7 @@ export type FieldComponent<
TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn,
TFormOnServer extends undefined | FormAsyncValidateOrFn,
TParentSubmitMeta,
+ ExtendedApi = {},
// This complex type comes from Vue's return type for `DefineSetupFnComponent` but with our own types sprinkled in
// This allows us to pre-bind some generics while keeping the props type unbound generics for props-based inferencing
> = new <
@@ -111,7 +112,8 @@ export type FieldComponent<
TFormOnSubmitAsync,
TFormOnServer,
TParentSubmitMeta
- >
+ > &
+ ExtendedApi
state: FieldApi<
TParentData,
TName,
diff --git a/packages/vue-form/src/useForm.tsx b/packages/vue-form/src/useForm.tsx
index 616fb0d58..d205b2a28 100644
--- a/packages/vue-form/src/useForm.tsx
+++ b/packages/vue-form/src/useForm.tsx
@@ -192,6 +192,45 @@ export interface VueFormApi<
>
}
+/**
+ * An extended version of the `FormApi` class that includes Vue-specific functionalities from `VueFormApi`
+ */
+export type VueFormExtendedApi<
+ TFormData,
+ TOnMount extends undefined | FormValidateOrFn,
+ TOnChange extends undefined | FormValidateOrFn,
+ TOnChangeAsync extends undefined | FormAsyncValidateOrFn,
+ TOnBlur extends undefined | FormValidateOrFn,
+ TOnBlurAsync extends undefined | FormAsyncValidateOrFn,
+ TOnSubmit extends undefined | FormValidateOrFn,
+ TOnSubmitAsync extends undefined | FormAsyncValidateOrFn,
+ TOnServer extends undefined | FormAsyncValidateOrFn,
+ TSubmitMeta,
+> = FormApi<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnServer,
+ TSubmitMeta
+> &
+ VueFormApi<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnServer,
+ TSubmitMeta
+ >
+
export function useForm<
TParentData,
TFormOnMount extends undefined | FormValidateOrFn,
diff --git a/packages/vue-form/tests/createFormComposition.test.tsx b/packages/vue-form/tests/createFormComposition.test.tsx
new file mode 100644
index 000000000..bdb39d934
--- /dev/null
+++ b/packages/vue-form/tests/createFormComposition.test.tsx
@@ -0,0 +1,86 @@
+import { describe, expect, it } from 'vitest'
+import { render } from '@testing-library/vue'
+import { defineComponent, h } from 'vue'
+import { createFormComposition, createFormCompositionContexts } from '../src'
+import type { AnyFieldApi } from '@tanstack/form-core'
+
+const { injectField, fieldProviderKey, formProviderKey, injectForm } =
+ createFormCompositionContexts()
+
+const TextField = defineComponent<{ label: string }>(
+ ({ label }) => {
+ const field = injectField()
+ return () => {
+ return (
+
+ )
+ }
+ },
+ {
+ props: ['label'],
+ },
+)
+
+const SubscribeButton = defineComponent<{ label: string }>(({ label }) => {
+ const form = injectForm()
+ return () => {
+ return (
+ state.isSubmitting}>
+ {(isSubmitting: boolean) => (
+
+ )}
+
+ )
+ }
+})
+
+const { useAppForm } = createFormComposition({
+ fieldComponents: {
+ TextField,
+ },
+ formComponents: {
+ SubscribeButton,
+ },
+ fieldProviderKey,
+ formProviderKey,
+})
+
+describe('createFormComposition', () => {
+ it('should allow to set default value', () => {
+ type Person = {
+ firstName: string
+ lastName: string
+ }
+
+ const Comp = defineComponent(() => {
+ const form = useAppForm({
+ defaultValues: {
+ firstName: 'FirstName',
+ lastName: 'LastName',
+ } as Person,
+ })
+
+ return () => (
+
+ {({ field }: { field: AnyFieldApi & Record<'TextField', any> }) => (
+
+ )}
+
+ )
+ })
+
+ const { getByLabelText } = render()
+ const input = getByLabelText('Testing')
+ expect(input).toHaveValue('FirstName')
+ })
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 742026abf..96fa9dd50 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -701,6 +701,28 @@ importers:
specifier: ^2.2.2
version: 2.2.8(typescript@5.8.2)
+ examples/vue/field-components:
+ dependencies:
+ '@tanstack/vue-form':
+ specifier: ^1.6.3
+ version: link:../../../packages/vue-form
+ vue:
+ specifier: ^3.5.13
+ version: 3.5.13(typescript@5.8.2)
+ devDependencies:
+ '@vitejs/plugin-vue':
+ specifier: ^5.2.3
+ version: 5.2.3(vite@6.3.2(@types/node@22.13.14)(jiti@2.4.2)(less@4.2.2)(sass@1.86.1)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))
+ typescript:
+ specifier: 5.8.2
+ version: 5.8.2
+ vite:
+ specifier: ^6.3.2
+ version: 6.3.2(@types/node@22.13.14)(jiti@2.4.2)(less@4.2.2)(sass@1.86.1)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
+ vue-tsc:
+ specifier: ^2.2.2
+ version: 2.2.8(typescript@5.8.2)
+
examples/vue/simple:
dependencies:
'@tanstack/vue-form':