form composition with custom validation
I'm currently doing forms with custom (per field) validation with zod, which works kinda ok. typical pattern is:
and until now, while verbose, i was kinda ok with that. but new form i'm working on needs to be split into sub components.
I have found the "choose your api" chart in https://tanstack.com/form/latest/docs/framework/react/guides/form-composition#api-usage-guidance
and it seems that it's not possible to continue using
useForm and custom validation functions with form composition, or am i mistaken?59 Replies
dependent-tanā¢5w ago
sort of.
useAppForm is a supserset of useForm, so none of your existing code breaks when switchingmanual-pinkOPā¢5w ago
@Luca | LeCarbonator thanks for your quick reply.
does that mean i "only need" to switch useAppForm and then use the hooks to get form context in my children?
dependent-tanā¢5w ago
So, say you have
useForm and you need to migrate to useAppForm. The process would roughly be:
* Create contexts, set up field/form components etc.
* Note that field/form components are on the UI layer, not the logic layer. Read up on withFieldGroup if you need field logic to be part of it too
* Change useForm to useAppForm
* When accessing form components, wrap it in <form.AppForm> (the context provider)
* Change fields that want field components from <form.Field> to <form.AppField>
Once everything's set up, it would look sort of like this:
manual-pinkOPā¢5w ago
may i ask why the need to
<field.StringInput /> ?dependent-tanā¢5w ago
StringInput would be a component accessing field context.
<field. just gives you an easy autocomplete if you like
if you instead want it to be through context only, you can pass it as child toomanual-pinkOPā¢5w ago
understood
dependent-tanā¢5w ago
that also goes for form components
manual-pinkOPā¢5w ago
i was trying to create a wrapper which would take
field as prop, but it seems quite challenging to type it properly. is there any type helper to do so?dependent-tanā¢5w ago
It depends on what you intend to do with the
field passed as propmanual-pinkOPā¢5w ago
probably making the whole <Field><FieldLabel> construction less verbose
dependent-tanā¢5w ago
as mentioned, since it's through context, you can compose sections together. For example, you can add
Input for granular control, but if you want a quick shortcut, you can wrap it in another field component called InputField
which would do
internallymanual-pinkOPā¢5w ago
i'm not sure to follow (with the api) š¤
i had in mind something from:
to
dependent-tanā¢5w ago
Here's what I meant:
manual-pinkOPā¢5w ago
mmm i see.. i understand the construct but it'd be quite verbose to create a wrapper component for each of them (as they belong to shadcn component library) ; and i can't either edit their source because some inputs are used outside of form validation (for search bars etc)
so i'd need to have a 1-1 mapping just for calling
useFieldContext as a hook
I could also do a HOC but i feel it's quite ugly :/
from {(field) => <Field><Input /></Field>}
to {withFieldWrapper((field) => <Input />)}dependent-tanā¢5w ago
{(field) => <InputField /> also works
also, most of the components don't actually rely on field context (like the label)
the biggest ones are the input and the field errorsmanual-pinkOPā¢5w ago
yes but for this i need to:
- create one InputField component with hook inside
- declare it as
fieldComponents.InputField
no?
i'm still confused as why do i need to do that if the current above (without fieldComponents and with (field) => ReactNode) seems like workingdependent-tanā¢5w ago
I'll ignore future features and PR drafts. Just looking at the current version of what
fieldComponents and formComponents does
it's mainly for intellisense. The form/field components are usually only used inside forms, so having an autocomplete to get the components you need makes it simpler. As far as runtime goes, you can import the component and use it directly too. Since it's passed with context, as long as you use <form.AppField>, you're good to go
in cases like this, you don't even need the field anymore. This would do too:
manual-pinkOPā¢5w ago
yes, that i understand. because InputComponent has a
useFieldContext() hook call inside itdependent-tanā¢5w ago
for your case, where you aren't sure if the input will be used for forms or for search bars, it could help distinguish it
instead of checking that you imported the correct
Field, you would know that everything under the <field. namespace is field-related
and with lazy-loading, you avoid adding that overhead to calls that don't need itmanual-pinkOPā¢5w ago
i think i get it.
in your reply i just noticed the nice comment about
fieldApi.parseValueWithSchema but then i need to pass a single validator inside, right? for example createUserSchema.shape.namedependent-tanā¢5w ago
yeah
it was initially added for combining callback validation with schema validation
manual-pinkOPā¢5w ago
there's no way for me to wrap this into a reusable function which would pick the shape[fieldApi.name] ?
dependent-tanā¢5w ago
but I just noticed that you don't use the callback part of it apart from parsing
If you are on zod version 3.24 or higher, you can pass it directly:
manual-pinkOPā¢5w ago
ok. it's one liner so i guess it's not that much
dependent-tanā¢5w ago
well, if you have it on form level, it will pass errors to the fields
so internally, it would do exactly that
manual-pinkOPā¢5w ago
yeah but my UX designer asked for "on blur individual field validation"
dependent-tanā¢5w ago
the form validator approach would be "all errors, but don't show them until the field is blurred or the user tried submitting"
manual-pinkOPā¢5w ago
so i can't validate the whole form with a common schema and display errors, that's why i thought of using individual onSubmit fns
dependent-tanā¢5w ago
which is usually a bit of boilerplate because of conditionally showing errors etc. Luckily, that is boilerplate that can be hidden inside field components
manual-pinkOPā¢5w ago
yeah i get that
so i could get rid of those individual onSubmit even if i needed to see only errors one by one based on if they're dirty or not...
mmm that's a lot to think about
dependent-tanā¢5w ago
We use that approach at our workplace if you'd like an example
manual-pinkOPā¢5w ago
i wouldn't mind of course but I don't want to use too much of your time
you've been tremendously helpful already
dependent-tanā¢5w ago
feedback's always helpful, especially since we're discussing helper types and the like
We used a common hook to have fields share common validation methods. It's not perfect yet (dependent fields sometimes don't show their errors until blurred or submitted), but it's quite nice as a start.
internally, we added a
Input component, which is just the boilerplate wrapper:
Then, we added a wrapper that contains it since we oftentimes want everything in one spot.
it's not great ... inputBefore and inputAfter were later additions, when we realized that our form fields aren't as consistently structured as they should be
I would have preferred granular control with common wrappers like the one above for the quick and easy route
Example usage:
as far as logic goes, we always add in the form schema into onChange. But visually, the user never sees errors until submission
It goes without saying I hope, but we don't use shadcn. You likely have to structure the UI a bit differentlymanual-pinkOPā¢5w ago
mmm i see
your InputField is what i was hoping for, but i didn't think of externalizing the logic as a hook. i would probably have done the 2 in 1 but use an open children slot with radix's Slottable to add props
we're also lucky that i've been enforcing a single interface for all kind of inputs which is
{ value:T, onChange: (newVal: T) => void } so i was looking into leveraging that to pass magically value={ field.state.value} and onChange={field.handleChange} so that childdependent-tanā¢5w ago
there's some plans for that, but I've only seen snippets and haven't checked it out myself.
The verbosity is part of being headless / framework agnostic. Allowing
value and onChange to be spreadable would be nice, but it would break some UI libraries and React Nativemanual-pinkOPā¢5w ago
yeah i understand talking to the world is another level x)
dependent-tanā¢5w ago
I do have an urge to refactor the components once more. As mentioned above, we're adding lots of props to cover edge cases when we really just should have let ourselves be granular sometimes
manual-pinkOPā¢5w ago
i'm currently picking a small form and trying to refactor as suggested using a single schema validator in
useForm({ validators{ onSubmit: createUserSchema } })
i'm running in a small quirk where one of the validator is expecting union of string | undefined but the defaultValues defines the field as string. should i just as string | undefined or is there a better way to handle this?dependent-tanā¢5w ago
it's an ongoing discussion, so I'll summarize a bit:
* We're trying to prevent deadlocks where schemas like zod assign errors to fields that never exist
* To do that, Standard Schema allows you to view the input type of schemas (zod:
z.input<typeof schema>)
* defaultValues should equal z.input<typeof schema>
There's some problems with that, since your default values are often not the desired outcome. Addressing that is a whole other story, but for now, defaultValues should be assigned z.input<typeof schema> so that errors become clearer
manual-pinkOPā¢5w ago
i guess maybe passing the schema as generic to
useForm rather than inferring from defaultValues would help :/ ?dependent-tanā¢5w ago
it would totally help, but TypeScript doesn't allow partial generics
so you either specify none, or you specify all ... 15 at this point? Not sure what the current count is
manual-pinkOPā¢5w ago
hahahaha
12 :p
dependent-tanā¢5w ago
i'm probably thinking of fields then
dependent-tanā¢5w ago
https://tanstack.com/form/latest/docs/philosophy#generics-are-grim here's a section talking about it too
Philosophy | TanStack Form Docs
Every well-established project should have a philosophy that guides its development. Without a core philosophy, development can languish in endless decision-making and have weaker APIs as a result. Th...
manual-pinkOPā¢5w ago
it's a very good design principle indeed
š
not sure if that's a big deal but this looks ok to me:
dependent-tanā¢5w ago
small nitpick,
z.infer is the output of the schema, not the inputmanual-pinkOPā¢5w ago
bonus point if i can:
oh nice catch thanks
dependent-tanā¢5w ago
zod allows you to view the input type, yeah. It differs heavily between v3 and v4, so I'm not sure if the v3 code is helpful
We use this helper:
manual-pinkOPā¢5w ago
ah so it's nullable but throws errors if so?
dependent-tanā¢5w ago
in this case, yes
z.input will be | null, but when calling schema.parse, it'll be non-nullablemanual-pinkOPā¢5w ago
aaah
zod v4 apparently has
z.output<T>dependent-tanā¢5w ago
probably slower than not making it nullable, but it's not the slowest part of our application so
always had it
manual-pinkOPā¢5w ago
so this can be done, no?
dependent-tanā¢5w ago
well, the defaultValues should be the input
output considers transforms as well
manual-pinkOPā¢5w ago
ok i misunderstood what you said earlier š indeed
z.input<T> is bestdependent-tanā¢5w ago

dependent-tanā¢5w ago
might as well be thorough

manual-pinkOPā¢5w ago
@Luca | LeCarbonator thanks again for this discussion š
dependent-tanā¢5w ago
no problem!