T
TanStack3w ago
deep-jade

typing `withForm`

Is this the correct way to type withForm?
import { useAppForm, withForm } from "~/hooks/form";

const PersonalInfoSection = withForm({
defaultValues: {
firstName: "",
lastName: "",
email: "",
address: {
street: "",
city: "",
zipCode: "",
},
},
render: ({ form }) => (
<div class="section">
<h3>Personal Information</h3>
<form.AppField name="firstName">
{(field) => <field.TextField label="First Name" />}
</form.AppField>
<form.AppField name="lastName">
{(field) => <field.TextField label="Last Name" />}
</form.AppField>
<form.AppField name="email">
{(field) => <field.TextField label="Email" />}
</form.AppField>
</div>
),
});

const AddressSection = withForm({
render: ({ form }) => (
<div class="section">
<h3>Address</h3>
<form.AppField name="address.street">
{(field) => <field.TextField label="Street" />}
</form.AppField>
<form.AppField name="address.city">
{(field) => <field.TextField label="City" />}
</form.AppField>
<form.AppField name="address.zipCode">
{(field) => <field.TextField label="ZIP Code" />}
</form.AppField>
</div>
),
});

// Main form component
export function MainForm() {
const form = useAppForm(() => ({
defaultValues: {
firstName: "",
lastName: "",
email: "",
address: {
street: "",
city: "",
zipCode: "",
},
},
onSubmit: async ({ value }) => {
console.log("Form submitted:", value);
},
}));

return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<PersonalInfoSection form={form} />
<AddressSection form={form} />

<form.AppForm>
<form.SubmitButton label="Submit Form" />
</form.AppForm>
</form>
);
}
import { useAppForm, withForm } from "~/hooks/form";

const PersonalInfoSection = withForm({
defaultValues: {
firstName: "",
lastName: "",
email: "",
address: {
street: "",
city: "",
zipCode: "",
},
},
render: ({ form }) => (
<div class="section">
<h3>Personal Information</h3>
<form.AppField name="firstName">
{(field) => <field.TextField label="First Name" />}
</form.AppField>
<form.AppField name="lastName">
{(field) => <field.TextField label="Last Name" />}
</form.AppField>
<form.AppField name="email">
{(field) => <field.TextField label="Email" />}
</form.AppField>
</div>
),
});

const AddressSection = withForm({
render: ({ form }) => (
<div class="section">
<h3>Address</h3>
<form.AppField name="address.street">
{(field) => <field.TextField label="Street" />}
</form.AppField>
<form.AppField name="address.city">
{(field) => <field.TextField label="City" />}
</form.AppField>
<form.AppField name="address.zipCode">
{(field) => <field.TextField label="ZIP Code" />}
</form.AppField>
</div>
),
});

// Main form component
export function MainForm() {
const form = useAppForm(() => ({
defaultValues: {
firstName: "",
lastName: "",
email: "",
address: {
street: "",
city: "",
zipCode: "",
},
},
onSubmit: async ({ value }) => {
console.log("Form submitted:", value);
},
}));

return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<PersonalInfoSection form={form} />
<AddressSection form={form} />

<form.AppForm>
<form.SubmitButton label="Submit Form" />
</form.AppForm>
</form>
);
}
7 Replies
deep-jade
deep-jadeOP3w ago
It feels weird to pass the same defaultValues twice. On PersonalInfoSection I don't have a TS error. On AddressSection I do. I understand why, it's just unclear to me if that's the way to do it. is the purpose of defaultValues in PersonalInfoSection to define the type without having an impact on the actual form values? should I do {} as MyType instead then?
flat-fuchsia
flat-fuchsia3w ago
the simplest way would be to create a shared formOptions() instead. withForm does a strict check to ensure only data that matches your form inside can be used. That includes validators, defaultValues and transforms. It doesn‘t use the defaultValues internally, so if you want to cast the type instead, that‘s fine. Just keep in mind that this is not true for field groups
flat-fuchsia
flat-fuchsia3w ago
there‘s actually a PR to make formOptions more useable now (because the typing was off before), so if you want to give that a spin to confirm it, you can find the install links here: https://github.com/TanStack/form/pull/1679
GitHub
fix(form-core): improve formOptions type preservation - WIP by a-is...
Add TFormData generic parameter to formOptions function Add test to verify listener type preservation with formOptions Current implementation has intentional type issues for investigation
deep-jade
deep-jadeOP3w ago
lgtm!
import { formOptions } from "@tanstack/solid-form";
import { useAppForm, withForm } from "~/hooks/form";

const options = formOptions({
defaultValues: {
firstName: "",
lastName: "",
email: "",
address: {
street: "",
city: "",
zipCode: "",
},
},
});

const PersonalInfoSection = withForm({
...options,
render: ({ form }) => (
<div class="section">
<h3>Personal Information</h3>
<form.AppField name="firstName">
{(field) => <field.TextField label="First Name" />}
</form.AppField>
<form.AppField name="lastName">
{(field) => <field.TextField label="Last Name" />}
</form.AppField>
<form.AppField name="email">
{(field) => <field.TextField label="Email" />}
</form.AppField>
</div>
),
});

const AddressSection = withForm({
...options,
render: ({ form }) => (
<div class="section">
<h3>Address</h3>
<form.AppField name="address.street">
{(field) => <field.TextField label="Street" />}
</form.AppField>
<form.AppField name="address.city">
{(field) => <field.TextField label="City" />}
</form.AppField>
<form.AppField name="address.zipCode">
{(field) => <field.TextField label="ZIP Code" />}
</form.AppField>
</div>
),
});

// Main form component
export function MainForm() {
const form = useAppForm(() => ({
...options,
onSubmit: async ({ value }) => {
console.log("Form submitted:", value);
},
}));

return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<PersonalInfoSection form={form} />
<AddressSection form={form} />

<form.AppForm>
<form.SubmitButton label="Submit Form" />
</form.AppForm>
</form>
);
}
import { formOptions } from "@tanstack/solid-form";
import { useAppForm, withForm } from "~/hooks/form";

const options = formOptions({
defaultValues: {
firstName: "",
lastName: "",
email: "",
address: {
street: "",
city: "",
zipCode: "",
},
},
});

const PersonalInfoSection = withForm({
...options,
render: ({ form }) => (
<div class="section">
<h3>Personal Information</h3>
<form.AppField name="firstName">
{(field) => <field.TextField label="First Name" />}
</form.AppField>
<form.AppField name="lastName">
{(field) => <field.TextField label="Last Name" />}
</form.AppField>
<form.AppField name="email">
{(field) => <field.TextField label="Email" />}
</form.AppField>
</div>
),
});

const AddressSection = withForm({
...options,
render: ({ form }) => (
<div class="section">
<h3>Address</h3>
<form.AppField name="address.street">
{(field) => <field.TextField label="Street" />}
</form.AppField>
<form.AppField name="address.city">
{(field) => <field.TextField label="City" />}
</form.AppField>
<form.AppField name="address.zipCode">
{(field) => <field.TextField label="ZIP Code" />}
</form.AppField>
</div>
),
});

// Main form component
export function MainForm() {
const form = useAppForm(() => ({
...options,
onSubmit: async ({ value }) => {
console.log("Form submitted:", value);
},
}));

return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<PersonalInfoSection form={form} />
<AddressSection form={form} />

<form.AppForm>
<form.SubmitButton label="Submit Form" />
</form.AppForm>
</form>
);
}
Im not sure what this PR improves in this example.
flat-fuchsia
flat-fuchsia3w ago
in this example, nothing. However, as soon as you attempt to add listeners and validators, the difference will show up if you don't use either one of those, then you needn't try the PR version
deep-jade
deep-jadeOP3w ago
Thank you for your help are you posting on bsky? you've been very helpful on every questions I had, I'm sure what you post would interest me!
flat-fuchsia
flat-fuchsia3w ago
no bsky, linkedin or twitter, I‘m afraid! I‘ve been mostly active on Discord since about 2018

Did you find this page helpful?