T
TanStack•3w ago
variable-lime

Form composition with zod validation

I'm attempting to compose a form with optional components, e.g. in a given form there may or may not be a component that collects customer information. I'd like to add zod validation for each of these components, and then I'll add some conditional logic to render the 'blocks' as needed. I'm attempting to add a second of these components (rental period) but I'm running into some types trouble with the onChange in the validator (fixed by not using z.union() as well as passing the form object as props to the form components. Sorry if the image is confusing, I can add more context if needed. Disclaimer I'm pretty new to React and Tanstack, so I may be conflating terms. Thanks for any help.
No description
16 Replies
variable-lime
variable-limeOP•3w ago
Maybe I misunderstood union. I put everything into one z.object() and the validation works now.
variable-lime
variable-limeOP•3w ago
I'm running into this error when passing the form object though. I'm not sure how to pass only part of the form to the component, which is what I assume the error is.
No description
xenial-black
xenial-black•3w ago
You're looking for the new formGroup feature CC @Luca | LeCarbonator (Luca is our resident form group expert and can help more here 🙂 )
variable-lime
variable-limeOP•3w ago
Wow, that was some timing. I was able to piece it together from the PR and fix the type issues. Just need to sort out submitting the form and then I'll be able to add the rest of my form components 🙂
correct-apricot
correct-apricot•3w ago
some advice before you start with groups: if you have optional blocks (could be undefined, could be present), then the field groups should reflect that. Otherwise, TypeScript may not realize that you‘re allowed to use the field group, even if you did the correct conditional check TL;DR conditional rendering is easier inside the field group than outside
variable-lime
variable-limeOP•3w ago
Thanks for the advice. So would I be passing props to these field groups that would control their rendering? As I'm attempting to map over a provided object that has what blocks should be available to render, because ordering will matter on the output form (I'm basically creating a form builder)
correct-apricot
correct-apricot•3w ago
no, I just mean that this type inference may have problems
type Foo = {
discriminator: true,
bar: string
} | {
discriminator: false,
}

// ...
<form.Field name="discriminator">
{field => field.state.value
// while TS infers that discriminator must be true,
// it likely won't infer that `bar` is therefore not
// null on the form instance.
? <BarFieldGroup form={form} fields={{ bar: "bar" }}/>
: null}
</form.Field>

// Fix: instead of expecting `bar: string` in withFieldGroup,
// expect string | undefined. Skip rendering if it's not string.

// For clarity and readability, you can keep the above assertion. It's only one
// additional if statement.
type Foo = {
discriminator: true,
bar: string
} | {
discriminator: false,
}

// ...
<form.Field name="discriminator">
{field => field.state.value
// while TS infers that discriminator must be true,
// it likely won't infer that `bar` is therefore not
// null on the form instance.
? <BarFieldGroup form={form} fields={{ bar: "bar" }}/>
: null}
</form.Field>

// Fix: instead of expecting `bar: string` in withFieldGroup,
// expect string | undefined. Skip rendering if it's not string.

// For clarity and readability, you can keep the above assertion. It's only one
// additional if statement.
this is dangerous with field groups this will cause a runtime error for fields="foo" because it needs to know that foo.X is a thing to generate the store and other stuff
frozen-sapphire
frozen-sapphire•3w ago
Interesting, I haven't seen that issue, but I'll watch for it. So far, it seems to be working. I'll remove my previous comment as not to cause issues. This is the only way I can get TS to not complain, though.
correct-apricot
correct-apricot•3w ago
the null and undefined is fine the {} as is not field rendering probably works, but the group‘s store would‘ve definitely broken
frozen-sapphire
frozen-sapphire•3w ago
Right, but I can't pass an object to it unless I just parse the schema directly and add in filler properties for the ones required. I thought the types would tell is that those fields existed similar to withForm. Under withForm, it says " // These values are only used for type-checking, and are not used at runtime" so maybe I'm misunderstanding that as well.
correct-apricot
correct-apricot•3w ago
and the field groups have a different comment
frozen-sapphire
frozen-sapphire•3w ago
Right, that says "// These default values are not used at runtime, but the keys are needed for mapping purposes. // This allows you to spread formOptions without needing to redeclare it."
correct-apricot
correct-apricot•3w ago
I guess this can be confusing perhaps I‘ll rephrase it to an alert widget and remove the „values“ part it‘s more correct, but at the cost of readability
frozen-sapphire
frozen-sapphire•3w ago
Well, you said it would give runtime errors so that means the object itself is used at runtime, right? Maybe clarify that? Or maybe it makes sense to others and I'm the odd man out, wouldn't be the first time So does that mean my object has to have ALL potential properties defaulted even if they're optional in that field group itself?
correct-apricot
correct-apricot•3w ago
perhaps it's easier to explain with what's going on behind the scenes for group.store, it checks for two things: * is fields a string? If so, use Object.keys(defaultValues) and prefix them wtih fields * if it's a field map, just use each string provided. Notice how if you do {} as Type, field maps likely still work, but fields="foo" would never give you proper store values.
// *thinks* it's string, but actually undefined, because {} has no keys
const bar = useStore(group.store, state => state.values.bar)
// *thinks* it's string, but actually undefined, because {} has no keys
const bar = useStore(group.store, state => state.values.bar)
So the values of the defaultValues are unused, but the keys are required.
frozen-sapphire
frozen-sapphire•3w ago
That makes a lot more sense. Ok, got it. I'll just have to define the object at the top and null out the optional values. Thank you!

Did you find this page helpful?