T
TanStack10mo ago
itchy-amethyst

Reusable Forms

Disclaimer: I think this came up already but I can't find it I have an Address-Form where I collect the Street, Postalcode and City of the user. I want to re-use the form for multiple purposes (Sign-Up, User-Profile, and Invoicing)
function AddressFormPart(
// <-- what should I pass here?
) {
return (
<>
<form.Field
name="address.street"
children={(field) => (
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>

<form.Field
name="address.city"
children={(field) => (
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>

<form.Field
name="address.postalCode"
children={(field) => (
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
</>
)
}

function SignUp() {
const form = useForm({
defaultValues: {
username: "",
// more data to be collected…
address: {
street: "",
city: "",
postalCode: "",
}
}
})

function handleSubmit() {}

return (
<form onSubmit={handleSubmit}>
<form.Field
name="username"
children={(field) => (
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
</form>
)
}
function AddressFormPart(
// <-- what should I pass here?
) {
return (
<>
<form.Field
name="address.street"
children={(field) => (
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>

<form.Field
name="address.city"
children={(field) => (
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>

<form.Field
name="address.postalCode"
children={(field) => (
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
</>
)
}

function SignUp() {
const form = useForm({
defaultValues: {
username: "",
// more data to be collected…
address: {
street: "",
city: "",
postalCode: "",
}
}
})

function handleSubmit() {}

return (
<form onSubmit={handleSubmit}>
<form.Field
name="username"
children={(field) => (
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
</form>
)
}
12 Replies
itchy-amethyst
itchy-amethystOP10mo ago
I'm also using validatorAdapter: zodValidator() - but wanted to keep it simple(r) in the example code
ambitious-aqua
ambitious-aqua10mo ago
GitHub
tanstack-boilerplate/src/components/ui/form.tsx at 44f012ed12b69d8b...
A fully type-safe boilerplate with a focus on UX and DX, complete with multiple examples. - nekochan0122/tanstack-boilerplate
itchy-amethyst
itchy-amethystOP10mo ago
Pascal Küsgen
StackBlitz
Form Simple Example (forked) - StackBlitz
Run official live example code for Form Simple, created by Tanstack on StackBlitz
notable-jade
notable-jade10mo ago
No description
notable-jade
notable-jade10mo ago
im making new api
itchy-amethyst
itchy-amethystOP10mo ago
I think what I'm after is the Reusable Form PARTS - so a couple of fields that can be re-used in a parent-form You're example looks like a "reusable form" (without the "part"). well that seems to make more problems than it solves… I think what I really need is a way to pass a part (partial) of the form as a FieldComponent or FormApi/ReactFormExtendedApi
<ReusableFormPart
Field={form.Field} // this works but the types don't match
// OR
form={form}
/>
<ReusableFormPart
Field={form.Field} // this works but the types don't match
// OR
form={form}
/>
Slapping some any on it "fixes" the type issues but then I'm not typesafe in the FormPart-Component anymore…
function FormPart({ form }: { form: ReactFormExtendedApi<any, ZodValidator> }) {
return (
<form.Field name="THIS-STRING-IS-NOT-TYPESAFE-ANYMORE" />
)
}
function FormPart({ form }: { form: ReactFormExtendedApi<any, ZodValidator> }) {
return (
<form.Field name="THIS-STRING-IS-NOT-TYPESAFE-ANYMORE" />
)
}
national-gold
national-gold9mo ago
This is how I managed to make it typesafe.
type ExtractFormData<T> = T extends ReactFormExtendedApi<infer U, any> ? U : never;

type InputProps<TFormData extends ReactFormExtendedApi<any, any>> = {
form: TFormData;
name: keyof ExtractFormData<TFormData>;
label: string;
};

const Input = <TFormData extends ReactFormExtendedApi<any, any>>({
form,
name,
label,
}: InputProps<TFormData>) => (
<form.Field name={name as string}>
{(field) => (
<>
<label htmlFor={field.name}>{label}</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
<FieldInfo field={field} />
</>
)}
</form.Field>
);
type ExtractFormData<T> = T extends ReactFormExtendedApi<infer U, any> ? U : never;

type InputProps<TFormData extends ReactFormExtendedApi<any, any>> = {
form: TFormData;
name: keyof ExtractFormData<TFormData>;
label: string;
};

const Input = <TFormData extends ReactFormExtendedApi<any, any>>({
form,
name,
label,
}: InputProps<TFormData>) => (
<form.Field name={name as string}>
{(field) => (
<>
<label htmlFor={field.name}>{label}</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
<FieldInfo field={field} />
</>
)}
</form.Field>
);
This results in:
<Input form={form} name="firstName" label="First Name" />
<Input form={form} name="firstName" label="First Name" />
absent-sapphire
absent-sapphire9mo ago
I wouldn't pass the whole form instance
national-gold
national-gold9mo ago
Is there a way to make name typesafe without passing the entire form instance?
absent-sapphire
absent-sapphire9mo ago
Can't you pass just form.field ?
itchy-amethyst
itchy-amethystOP9mo ago
<form.Field name={name as string}> casting to string seems suspicious… You're basically telling Typescript here "It's fine, it's a string - trust me", right? So if you pass "foobar" it wouldn't show up as an error in your IDE?
national-gold
national-gold9mo ago
The typesafety is on the root name variable not the one on Field. So when you are writing <Input form={form} name="" /> name will be typesafe based on what you passed into useForm Another way of doing this I'm experimenting with is:
<form.Field name="firstName">
{(field) => <Input field={field} label="First Name" />}
</form.Field>
<form.Field name="firstName">
{(field) => <Input field={field} label="First Name" />}
</form.Field>

Did you find this page helpful?