Powerful, type-safe forms for React.
Advanced features for creating custom form hooks with component injection, React contexts for form and field data, higher-order components, and utility functions for form manipulation and helper operations.
Creates a custom form hook with injected field and form components.
/**
* Creates a custom form hook with extended field and form components
* Enables component injection pattern for reusable form UI components
*
* @param props.fieldComponents - Custom field-level components to inject
* @param props.fieldContext - React context for field data
* @param props.formContext - React context for form data
* @param props.formComponents - Custom form-level components to inject
* @returns Object containing useAppForm, withForm, and withFieldGroup functions
*/
function createFormHook<
const TComponents extends Record<string, ComponentType<any>>,
const TFormComponents extends Record<string, ComponentType<any>>,
>({
fieldComponents,
fieldContext,
formContext,
formComponents,
}: CreateFormHookProps<TComponents, TFormComponents>): {
/**
* Custom form hook with injected components
* Similar to useForm but returns AppFieldExtendedReactFormApi with custom components
*/
useAppForm: <
TFormData,
TOnMount extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnChange extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
TOnBlur extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
TOnSubmit extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
TOnDynamic extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
TSubmitMeta = never,
>(
props: FormOptions<
TFormData,
TOnMount,
TOnChange,
TOnChangeAsync,
TOnBlur,
TOnBlurAsync,
TOnSubmit,
TOnSubmitAsync,
TOnDynamic,
TOnDynamicAsync,
TOnServer,
TSubmitMeta
>,
) => AppFieldExtendedReactFormApi<
TFormData,
TOnMount,
TOnChange,
TOnChangeAsync,
TOnBlur,
TOnBlurAsync,
TOnSubmit,
TOnSubmitAsync,
TOnDynamic,
TOnDynamicAsync,
TOnServer,
TSubmitMeta,
TComponents,
TFormComponents
>;
/**
* Higher-order component for wrapping forms
* Provides form instance to render function
*/
withForm: <
TFormData,
TOnMount extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnChange extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
TOnBlur extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
TOnSubmit extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
TOnDynamic extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
TSubmitMeta = never,
TRenderProps extends object = {},
>(
props: WithFormProps<
TFormData,
TOnMount,
TOnChange,
TOnChangeAsync,
TOnBlur,
TOnBlurAsync,
TOnSubmit,
TOnSubmitAsync,
TOnDynamic,
TOnDynamicAsync,
TOnServer,
TSubmitMeta,
TComponents,
TFormComponents,
TRenderProps
>,
) => (props: PropsWithChildren<TRenderProps>) => JSX.Element;
/**
* Higher-order component for wrapping field groups
* Provides field group instance to render function
*/
withFieldGroup: <
TFieldGroupData,
TSubmitMeta = never,
TRenderProps extends Record<string, unknown> = {},
>(
props: WithFieldGroupProps<
TFieldGroupData,
TComponents,
TFormComponents,
TSubmitMeta,
TRenderProps
>,
) => <
TFormData,
TFields extends
| DeepKeysOfType<TFormData, TFieldGroupData | null | undefined>
| FieldsMap<TFormData, TFieldGroupData>,
TOnMount extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnChange extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
TOnBlur extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
TOnSubmit extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
TOnDynamic extends undefined | FormValidateOrFn<TFormData> = undefined,
TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
>(
params: PropsWithChildren<
TRenderProps & {
form: AppFieldExtendedReactFormApi<...>;
fields: TFields;
}
>,
) => JSX.Element;
};
interface CreateFormHookProps<
TFieldComponents extends Record<string, ComponentType<any>>,
TFormComponents extends Record<string, ComponentType<any>>,
> {
/** Custom field-level components (e.g., custom input wrappers) */
fieldComponents: TFieldComponents;
/** React context for accessing field data */
fieldContext: Context<AnyFieldApi>;
/** Custom form-level components (e.g., custom form wrapper) */
formComponents: TFormComponents;
/** React context for accessing form data */
formContext: Context<AnyFormApi>;
}Creates React contexts and hooks for accessing form and field data.
/**
* Creates React contexts and hooks for form and field data access
* Use this to create contexts before calling createFormHook
*
* @returns Object containing contexts and hook functions
*/
function createFormHookContexts(): {
/** React context for field data */
fieldContext: Context<AnyFieldApi>;
/**
* Hook to access field context within a field component
* @returns Current field API instance
* @throws Error if called outside of field component context
*/
useFieldContext: <TData>() => FieldApi<
any,
string,
TData,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any
>;
/**
* Hook to access form context within a form component
* @returns Current form API instance
* @throws Error if called outside of form component context
*/
useFormContext: () => ReactFormExtendedApi<
Record<string, never>,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any
>;
/** React context for form data */
formContext: Context<AnyFormApi>;
};Props interface for withForm higher-order component.
interface WithFormProps<
TFormData,
TOnMount extends undefined | FormValidateOrFn<TFormData>,
TOnChange extends undefined | FormValidateOrFn<TFormData>,
TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnBlur extends undefined | FormValidateOrFn<TFormData>,
TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
TSubmitMeta,
TFieldComponents extends Record<string, ComponentType<any>>,
TFormComponents extends Record<string, ComponentType<any>>,
TRenderProps extends object = Record<string, never>,
> extends FormOptions<
TFormData,
TOnMount,
TOnChange,
TOnChangeAsync,
TOnBlur,
TOnBlurAsync,
TOnSubmit,
TOnSubmitAsync,
TOnDynamic,
TOnDynamicAsync,
TOnServer,
TSubmitMeta
> {
/** Optional additional props for render function */
props?: TRenderProps;
/**
* Render function that receives form instance and props
* @param props - Combined props with form API
* @returns JSX element
*/
render: (
props: PropsWithChildren<
TRenderProps & {
form: AppFieldExtendedReactFormApi<
TFormData,
TOnMount,
TOnChange,
TOnChangeAsync,
TOnBlur,
TOnBlurAsync,
TOnSubmit,
TOnSubmitAsync,
TOnDynamic,
TOnDynamicAsync,
TOnServer,
TSubmitMeta,
TFieldComponents,
TFormComponents
>;
}
>,
) => JSX.Element;
}Props interface for withFieldGroup higher-order component.
interface WithFieldGroupProps<
TFieldGroupData,
TFieldComponents extends Record<string, ComponentType<any>>,
TFormComponents extends Record<string, ComponentType<any>>,
TSubmitMeta,
TRenderProps extends Record<string, unknown> = Record<string, never>,
> extends BaseFormOptions<TFieldGroupData, TSubmitMeta> {
/** Optional additional props for render function */
props?: TRenderProps;
/**
* Render function that receives field group instance and props
* @param props - Combined props with field group API
* @returns JSX element
*/
render: (
props: PropsWithChildren<
TRenderProps & {
group: AppFieldExtendedReactFieldGroupApi<
unknown,
TFieldGroupData,
string | FieldsMap<unknown, TFieldGroupData>,
undefined | FormValidateOrFn<unknown>,
undefined | FormValidateOrFn<unknown>,
undefined | FormAsyncValidateOrFn<unknown>,
undefined | FormValidateOrFn<unknown>,
undefined | FormAsyncValidateOrFn<unknown>,
undefined | FormValidateOrFn<unknown>,
undefined | FormAsyncValidateOrFn<unknown>,
undefined | FormValidateOrFn<unknown>,
undefined | FormAsyncValidateOrFn<unknown>,
undefined | FormAsyncValidateOrFn<unknown>,
unknown extends TSubmitMeta ? never : TSubmitMeta,
TFieldComponents,
TFormComponents
>;
}
>,
) => JSX.Element;
}Extended form API with custom components.
type AppFieldExtendedReactFormApi<
TFormData,
TOnMount extends undefined | FormValidateOrFn<TFormData>,
TOnChange extends undefined | FormValidateOrFn<TFormData>,
TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnBlur extends undefined | FormValidateOrFn<TFormData>,
TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
TSubmitMeta,
TFieldComponents extends Record<string, ComponentType<any>>,
TFormComponents extends Record<string, ComponentType<any>>,
> = ReactFormExtendedApi<
TFormData,
TOnMount,
TOnChange,
TOnChangeAsync,
TOnBlur,
TOnBlurAsync,
TOnSubmit,
TOnSubmitAsync,
TOnDynamic,
TOnDynamicAsync,
TOnServer,
TSubmitMeta
> &
TFormComponents & {
/** Field component with custom field components injected */
AppField: FieldComponent<
TFormData,
TOnMount,
TOnChange,
TOnChangeAsync,
TOnBlur,
TOnBlurAsync,
TOnSubmit,
TOnSubmitAsync,
TOnDynamic,
TOnDynamicAsync,
TOnServer,
TSubmitMeta,
TFieldComponents
>;
/** Form wrapper component with context provider */
AppForm: ComponentType<PropsWithChildren>;
};/**
* Applies an updater function or value to an input
* @param updater - Function or value to apply
* @param input - Input value
* @returns Updated value
*/
function functionalUpdate<TInput, TOutput = TInput>(
updater: Updater<TInput, TOutput>,
input: TInput,
): TOutput;
/**
* Merges partial state into a form instance
* @param baseForm - Base form API instance
* @param state - Partial state to merge
* @returns Form API with merged state
*/
function mergeForm<TFormData>(
baseForm: FormApi<TFormData, ...>,
state: Partial<FormApi<TFormData, ...>['state']>,
): FormApi<TFormData, ...>;
/**
* Deep merges source object into target object (mutating)
* @param target - Target object to merge into
* @param source - Source object to merge from
* @returns Merged object
*/
function mutateMergeDeep(
target: object | null | undefined,
source: object | null | undefined,
): object;/**
* Gets a value from an object using a path
* Supports dot notation and array indices
* @param obj - Object to traverse
* @param path - Path string or array
* @returns Value at path
*/
function getBy(obj: any, path: any): any;
/**
* Sets a value on an object using a path
* Supports dot notation and array indices
* @param obj - Object to update
* @param path - Path string or array
* @param updater - Value or function to set
* @returns Updated object
*/
function setBy(obj: any, path: any, updater: Updater<any>): any;
/**
* Deletes a field on an object using a path
* @param obj - Object to update
* @param path - Path to delete
* @returns Updated object
*/
function deleteBy(obj: any, path: any): any;
/**
* Converts a path string to an array of path segments
* @param str - Path string or array
* @returns Array of path segments
*/
function makePathArray(str: string | Array<string | number>): Array<string | number>;
/**
* Concatenates two paths together
* @param path1 - First path
* @param path2 - Second path
* @returns Concatenated path string
*/
function concatenatePaths(path1: string, path2: string): string;/**
* Creates type-safe form options with proper inference
* @param defaultOpts - Form options with custom properties
* @returns Type-safe form options
*/
function formOptions<TOptions, TFormData, ...>(
defaultOpts: Partial<FormOptions<TFormData, ...>> & TOptions,
): TOptions;
/**
* Creates a map of field keys to their names
* @param values - Object with field values
* @returns Map of keys to names
*/
function createFieldMap<T>(values: Readonly<T>): { [K in keyof T]: K };
/**
* Checks if a value is a non-empty array
* @param obj - Value to check
* @returns True if value is non-empty array
*/
function isNonEmptyArray(obj: any): boolean;
/**
* Generates a unique identifier
* @returns Unique ID string
*/
function uuid(): string;
/**
* Merges default options with provided options
* @param defaultOpts - Default options
* @param opts - Override options
* @returns Merged options
*/
function mergeOpts<T>(defaultOpts: T, opts?: T): T;
/**
* Deep equality check for two objects
* @param objA - First object
* @param objB - Second object
* @returns True if objects are deeply equal
*/
function evaluate<T>(objA: T, objB: T): boolean;/**
* Default field metadata object
*/
const defaultFieldMeta: AnyFieldMeta;
/**
* Helper function for managing field metadata during array operations
* @param formApi - The form API instance
* @returns Object with handleArrayFieldMetaShift method for managing field metadata
*/
function metaHelper<TFormData, ...>(
formApi: FormApi<TFormData, ...>
): {
handleArrayFieldMetaShift: (
field: DeepKeys<TFormData>,
index: number,
mode: 'insert' | 'remove' | 'swap' | 'move',
secondIndex?: number
) => void;
};import { createFormHook, createFormHookContexts } from '@tanstack/react-form';
// Create contexts
const contexts = createFormHookContexts();
// Define custom components
const fieldComponents = {
// Custom input component with built-in styling
Input: ({ ...props }) => (
<input
className="custom-input"
{...props}
/>
),
// Custom select component
Select: ({ options, ...props }) => (
<select className="custom-select" {...props}>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
),
};
const formComponents = {
// Custom form wrapper
Card: ({ children }) => (
<div className="card">
<div className="card-body">{children}</div>
</div>
),
};
// Create custom hook
const { useAppForm, withForm, withFieldGroup } = createFormHook({
fieldComponents,
fieldContext: contexts.fieldContext,
formComponents,
formContext: contexts.formContext,
});
// Use the custom form hook
function MyForm() {
const form = useAppForm({
defaultValues: {
name: '',
color: 'red',
},
});
return (
<form.AppForm>
<form.Card>
<form.AppField name="name">
{(field) => (
<div>
<label>Name:</label>
<field.Input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
</div>
)}
</form.AppField>
<form.AppField name="color">
{(field) => (
<div>
<label>Color:</label>
<field.Select
options={[
{ value: 'red', label: 'Red' },
{ value: 'blue', label: 'Blue' },
]}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
</div>
)}
</form.AppField>
</form.Card>
</form.AppForm>
);
}const { withForm } = createFormHook({
fieldComponents: {},
fieldContext: contexts.fieldContext,
formComponents: {},
formContext: contexts.formContext,
});
const ContactForm = withForm({
defaultValues: {
name: '',
email: '',
},
validators: {
onChange: ({ value }) => {
if (!value.email.includes('@')) {
return 'Invalid email';
}
return undefined;
},
},
onSubmit: async ({ value }) => {
await submitToServer(value);
},
render: ({ form }) => (
<form.AppForm>
<form.AppField name="name">
{(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
</form.AppField>
<form.AppField name="email">
{(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
</form.AppField>
<button type="submit">Submit</button>
</form.AppForm>
),
});
// Use the component
function App() {
return <ContactForm />;
}const { withFieldGroup } = createFormHook({
fieldComponents: {},
fieldContext: contexts.fieldContext,
formComponents: {},
formContext: contexts.formContext,
});
const AddressInput = withFieldGroup({
defaultValues: {
street: '',
city: '',
state: '',
zip: '',
},
render: ({ group }) => (
<div className="address-group">
<group.Field name="street">
{(field) => (
<input
placeholder="Street"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
</group.Field>
<group.Field name="city">
{(field) => (
<input
placeholder="City"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
</group.Field>
<group.Field name="state">
{(field) => (
<input
placeholder="State"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
</group.Field>
<group.Field name="zip">
{(field) => (
<input
placeholder="ZIP"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
</group.Field>
</div>
),
});
// Use in a form
function UserForm() {
const form = useAppForm({
defaultValues: {
name: '',
shippingAddress: {
street: '',
city: '',
state: '',
zip: '',
},
billingAddress: {
street: '',
city: '',
state: '',
zip: '',
},
},
});
return (
<form.AppForm>
<h2>Shipping Address</h2>
<AddressInput form={form} fields="shippingAddress" />
<h2>Billing Address</h2>
<AddressInput form={form} fields="billingAddress" />
</form.AppForm>
);
}import { createFormHookContexts } from '@tanstack/react-form';
const { useFieldContext, useFormContext } = createFormHookContexts();
// Custom field wrapper component
function FieldWrapper({ children }) {
const field = useFieldContext<string>();
return (
<div className="field-wrapper">
<div className="field-content">{children}</div>
{field.state.meta.errors[0] && (
<div className="field-error">{field.state.meta.errors[0]}</div>
)}
</div>
);
}
// Custom submit button component
function SubmitButton() {
const form = useFormContext();
return (
<button
type="submit"
disabled={!form.state.canSubmit || form.state.isSubmitting}
>
{form.state.isSubmitting ? 'Submitting...' : 'Submit'}
</button>
);
}import { getBy, setBy, concatenatePaths } from '@tanstack/react-form';
function DynamicFieldName() {
const form = useForm({
defaultValues: {
user: {
profile: {
firstName: 'John',
},
},
},
});
const basePath = 'user.profile';
const fieldName = 'firstName';
const fullPath = concatenatePaths(basePath, fieldName);
// Get value using path
const value = getBy(form.state.values, fullPath);
console.log(value); // 'John'
// Set value using path
const updated = setBy(
form.state.values,
fullPath,
(current) => current.toUpperCase()
);
return <div>{value}</div>;
}Event client for integrating with TanStack Form DevTools for debugging and monitoring form state.
/**
* Event client instance for form devtools integration
* Used internally by FormApi to broadcast form state changes
*/
const formEventClient: FormEventClient;
/**
* Form state change broadcast event payload
*/
type BroadcastFormState = {
/** Unique form identifier */
id: string;
/** Current form state */
state: AnyFormState;
/** Form options */
options: AnyFormOptions;
};
/**
* Form submission state change broadcast event payload
*/
type BroadcastFormSubmissionState =
| {
id: string;
submissionAttempt: number;
successful: false;
stage: 'validateAllFields' | 'validate';
errors: any[];
}
| {
id: string;
submissionAttempt: number;
successful: false;
stage: 'inflight';
onError: unknown;
}
| {
id: string;
submissionAttempt: number;
successful: true;
};
/**
* Form unmounted event payload
*/
type BroadcastFormUnmounted = {
/** Form identifier that was unmounted */
id: string;
};
/**
* Request form state event payload
*/
type RequestFormState = {
/** Form identifier to request state for */
id: string;
};
/**
* Request form reset event payload
*/
type RequestFormReset = {
/** Form identifier to reset */
id: string;
};
/**
* Request form force reset event payload
*/
type RequestFormForceReset = {
/** Form identifier to force reset */
id: string;
};
/**
* Event client event map type
* Maps event names to their payload types
*/
type EventClientEventMap = keyof {
'form-devtools:form-state-change': BroadcastFormState;
'form-devtools:form-submission-state-change': BroadcastFormSubmissionState;
'form-devtools:form-unmounted': BroadcastFormUnmounted;
'form-devtools:request-form-state': RequestFormState;
'form-devtools:request-form-reset': RequestFormReset;
'form-devtools:request-form-force-submit': RequestFormForceReset;
};
/**
* Extracted event names from event client event map
*/
type EventClientEventNames =
| 'form-state-change'
| 'form-submission-state-change'
| 'form-unmounted'
| 'request-form-state'
| 'request-form-reset'
| 'request-form-force-submit';Note: The Event Client is primarily for internal use by TanStack Form DevTools. Most applications do not need to interact with it directly.
Install with Tessl CLI
npx tessl i tessl/npm-tanstack--react-form