Form Context
Foundation contexts and consumer hooks for building type-safe TanStack Form components via createFormHook.
npx shadcn@latest add https://shuip.plvo.dev/r/tsf-form-context.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/tsf-form-context.json
bun x shadcn@latest add https://shuip.plvo.dev/r/tsf-form-context.json
'use client';import { createFormHookContexts } from '@tanstack/react-form';export const { fieldContext, formContext, useFieldContext, useFormContext } = createFormHookContexts();
form-context exports the React contexts and consumer hooks produced by TanStack Form's createFormHookContexts(). It is the foundation for the context-bound composition pattern: reusable field and form components read their state from React context instead of receiving the form instance as a prop.
This sidesteps the variance problem that arises when ReactFormApi's eleven validator generics need to cross a component boundary — the same problem that forces form={form as any} casts in the prop-drilling pattern.
What you install
// components/ui/shuip/tanstack-form/form-context.tsx
'use client';
import { createFormHookContexts } from '@tanstack/react-form';
export const { fieldContext, formContext, useFieldContext, useFormContext } = createFormHookContexts();Four exports:
fieldContext,formContext— pass these tocreateFormHookin your project setup.useFieldContext<T>()— call inside a field component to readfield(state, errors, handlers). The generic asserts the field value type.useFormContext()— call inside a form-level component (submit buttons, layout chrome) to read the form instance.
Project setup
Create a lib/form.ts in your project that calls createFormHook once, binding every field and form component you want available globally.
// lib/form.ts
import { createFormHook } from '@tanstack/react-form';
import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context';
import { TextField } from '@/components/ui/my-text-field';
import { SubmitButton } from '@/components/ui/my-submit-button';
export const { useAppForm, withForm, withFieldGroup } = createFormHook({
fieldContext,
formContext,
fieldComponents: { TextField },
formComponents: { SubmitButton },
});Consumer usage:
import { useAppForm } from '@/lib/form';
import { z } from 'zod';
const schema = z.object({ username: z.string().min(3) });
function MyForm() {
const form = useAppForm({
defaultValues: { username: '' },
validators: { onChange: schema },
onSubmit: ({ value }) => console.log(value),
});
return (
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
<form.AppField name='username' children={(field) => <field.TextField label='Username' />} />
<form.AppForm><form.SubmitButton>Submit</form.SubmitButton></form.AppForm>
</form>
);
}The name is autocompleted from defaultValues. Validators accept any standard schema (Zod, Valibot, ArkType) or a plain function.
Building a field component
A field component consumes the field via useFieldContext. It never receives form or name as a prop — both come from the surrounding <form.AppField>.
'use client';
import { useFieldContext } from '@/components/ui/shuip/tanstack-form/form-context';
import { Input } from '@/components/ui/input';
export function TextField({ label }: { label: string }) {
const field = useFieldContext<string>();
return (
<label>
{label}
<Input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
aria-invalid={!field.state.meta.isValid}
/>
</label>
);
}Example
Default
'use client';import { createFormHook } from '@tanstack/react-form';import { z } from 'zod';import { Button } from '@/components/ui/button';import { Field, FieldError, FieldLabel } from '@/components/ui/field';import { Input } from '@/components/ui/input';import {fieldContext,formContext,useFieldContext,useFormContext,} from '@/components/ui/shuip/tanstack-form/form-context';function TextField({ label }: { label: string }) {const field = useFieldContext<string>();const { isValid, errors } = field.state.meta;return (<Field data-invalid={!isValid}><FieldLabel htmlFor={field.name}>{label}</FieldLabel><Inputid={field.name}name={field.name}value={field.state.value}onChange={(e) => field.handleChange(e.target.value)}onBlur={field.handleBlur}aria-invalid={!isValid}/>{!isValid && (<FieldError errors={errors.map((error) => ({ message: typeof error === 'string' ? error : error?.message }))} />)}</Field>);}function SubmitButton({ children }: { children: React.ReactNode }) {const form = useFormContext();return (<form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]}>{([canSubmit, isSubmitting]) => (<Button type='submit' disabled={!canSubmit}>{isSubmitting ? 'Submitting…' : children}</Button>)}</form.Subscribe>);}const { useAppForm } = createFormHook({fieldContext,formContext,fieldComponents: { TextField },formComponents: { SubmitButton },});const schema = z.object({username: z.string().min(3, 'Username must be at least 3 characters'),});export default function FormContextExample() {const form = useAppForm({defaultValues: { username: '' },validators: { onChange: schema },onSubmit: async ({ value }) => {alert(JSON.stringify(value, null, 2));},});return (<formonSubmit={(e) => {e.preventDefault();form.handleSubmit();}}className='flex flex-col gap-3'><form.AppField name='username' children={(field) => <field.TextField label='Username' />} /><form.AppForm><form.SubmitButton>Submit</form.SubmitButton></form.AppForm></form>);}
Exports
Prop
Type