Form Validation scope
Is the scope of form.store.state.isValid for the whole form including all withForm components or is it for the currently mounted component? I'm trying to find a way to track which steps have errors by setting a Zustand property if that mounted withForm component has any field errors without having to check every single field. So I was hoping to use the isValid property from the store state via useStore. But if that's for all fields across all components, then it's not as useful.
What I'm doing is using field level validations of onSubmit and onChange. I'm probably going to change the onSubmit to onBlur instead. So if there's an error, the user will know right away and that should update the form isValid to false. But again, if that's for the whole form regardless of mounted withForm component, I'll need to find an alternative.
33 Replies
absent-sapphireOP•2mo ago
@Luca | LeCarbonator Can you possibly answer this?
cloudy-cyan•2mo ago
it's for all fields across all components
but mounted components vs. unmounted components can make a difference when it comes to field validators / errors, so it's hard to tell what the state will be in practice
if you have form validators, then they will consider all fields, so those will be considered at any step in the form
absent-sapphireOP•2mo ago
I was more concerned with the form state tracking errors from field validators. But from what you say, it sounds like if a field sets an error, when that component is unmounted while the error persists, then the error on the form state would stay.
cloudy-cyan•2mo ago
... I think? I hope ... this is worth testing actually
absent-sapphireOP•2mo ago
Well, once I'm done with this refactor, I'll do just that. It'll be a while still. I'm having to get creative how I update different things as my forms have a header that has buttons that does stuff. Right now I'm just passing props to set fields using form.setFieldValue. But anyway, I'll report back.
cloudy-cyan•2mo ago
Great! Your feedback's been a huge help so far, so don't feel pressured to rush it!
absent-sapphireOP•2mo ago
@Luca | LeCarbonator One more somewhat related question - if I call useStore outside of the form components, will it still return the current form information? Like if I wanted to see if it's valid in a header or footer component that sits above/below the form component itself?
cloudy-cyan•2mo ago
and how would you access the store for use in that hook?
you mean on the same level as the useForm hook?
absent-sapphireOP•2mo ago
Oh fair...I see what you mean because we usually call it like useStore(form.store, (state) => ....). I'm not 100% certain I even need the access to that where I'm wanting to call it. Ok, ignore that question.
cloudy-cyan•2mo ago
I can still sort of answer it. If you have access to
form.store
, yes it will be up to date with the selector. Note that there‘s a tracking issue for Date and File objects, but the rest should work as expected
the form from useForm is a stable reference, so it should be fine to elevate it and using withForm
to access it in the componentabsent-sapphireOP•2mo ago
What I was thinking was that I could have a reset button on the footer of my form, but since that footer component doesn't use withForm, it wouldn't have access to it. Do I have to now make this component use that hook? Or do I have to just pass down the function via props?
cloudy-cyan•2mo ago
well, depends on your ARIA behaviour
reset buttons would usually belong inside form html elements, not outside. However, you would end up calling preventDefault anyways for your behaviour, so they could be outside and simply pass the onClick as onReset to the parent component
absent-sapphireOP•2mo ago
Well, it's technically (from the user perspective) inside the form, but structurally it is outside of the actual form component. Maybe I'm not setting it up the best way so I'll look at that and see if I can refactor that somehow.
I guess what I'm struggling with is my main Form component is really just a wrapper around the steps. I originally wanted it to be the actual form, but since I have multiple types that are using this form, I couldn't. The current functionality struggles with discriminated unions. So I split up the union into their own respective forms that wrap the Form component which holds the Header and Footer component. I guess Ic an just pass the form api methods I need as props, it just feels tedious is all.
cloudy-cyan•2mo ago
GitHub
[Feature Request]: Form Groups · Issue #419 · TanStack/form
Description When building a form stepper, like so: It's common for each step to have its own form. However, this complicates the form submission and validation process by requiring you to add c...
cloudy-cyan•2mo ago
we‘re not sure what a convenient API would be for it
absent-sapphireOP•2mo ago
Yep. I ended up taking a step back. I made each step their own useForm since I’m already updating Zustand on blur via listeners. This allowed me to do individual step validation as well as doing a reset for each step without resetting the full form. I register each form’s reset and isValid in my store. I know this doesn’t solve it for everyone if they’re not using a store. But this made it easier than attempting to use useAppForm and having each step as a withForm component.
But by doing that, I enabled my header steps to have some custom functionality based on that steps isValid and allowed the footer the same as well as being able to have a reset button.
I will say that I like Tanstack Form (I never used RHF so can’t compare to that) but I think it’s missing a lot of things that would add so much more. Things like being able to type the form prop to pass through components that aren’t a withForm or withFieldGroup component, better handling of discriminated unions, better handling of optional properties. It’s not unusable at all, but takes a bit more work in its current state.
cloudy-cyan•2mo ago
the goals of TSF are listed in the philosophy page, but something to consider:
* Type changes are not considered breaking changes, so allowing users to freely 'play' with them can have disastrous consequences, especially since it's more often than not something you can implement and optimize in TSF itself
* The end goal of TSF is to be type safe, but require as little passing of types as possible. With TypeScript not having partial inheritance for generics, it will most likely stick to higher order components like
withForm
and withFieldGroup
.
of course you end up limiting exploration as well with it. But if possible, we don't want to end up in situations like RHF's controllersabsent-sapphireOP•2mo ago
@Luca | LeCarbonator Can you update the group.reset() method to accept values similar to the form.reset? That would help out for odd cases like mine where I need the default values to be a discriminated union, but I can't parse them at render right away due to the discriminator being a prop on the group, so I was going to do a reset in a useEffect on mount to fix that. But I noticed it doesn't accept values.
I updated it locally on my side real quick using the form.reset structure and updating the values type to be the TFieldGroupData type
cloudy-cyan•2mo ago
that's why it's not allowed to pass values like that to the group's reset, because the form that uses it can be of any structure
absent-sapphireOP•2mo ago
Hmmm, I see your point
cloudy-cyan•2mo ago
but it's easy to make it form-specific, by adding a custom
onReset
prop
that receives the values and the form is responsible to manage the reset behaviourabsent-sapphireOP•2mo ago
Yeah...but I don't need to reset the whole form, just that group specifically to be able to parse the discriminated union. The group has all the same fields mostly and I'd hate to spit it for just like one or two small differences.
So, if the form is responsible, why does the group have a reset method at all then? If we type the values to be what the TFieldGroupData should be, shouldn't that work just the same since it can't have fields that aren't allowed based on the defaultValue you definition?
cloudy-cyan•2mo ago
because you may want to revert the form back to its default values.
meanwhile, resetting to new values would require you to know the structure of the form that uses your group ahead of time, which goes against what
withFieldGroup
is trying to accomplish. withForm
would be the appropriate use case for thatabsent-sapphireOP•2mo ago
Right, what I'm saying is why does the group object have a reset method if the form is the one controlling it? Is it just calling the form's reset?
cloudy-cyan•2mo ago
it is
absent-sapphireOP•2mo ago
Ah
cloudy-cyan•2mo ago
actually … yeah, good point
it should be group.form.reset then
I see the misunderstanding now. I‘ll remove that to make it clearer that it‘s the form resetting and not the group. Good catch @ExNihilo !
resetting partial defaultValues has been on my mind too recently. Perhaps it will be implemented one day.
how about changing
resetField
to allow an override value?absent-sapphireOP•2mo ago
Oh, I think I'm also thinking about this incorrectly. I see what you're saying about the form controlling which makes more sense once I move my thinking up a level. The default values for the group aren't actually set, they're just there for typescript similar to withForm. So doing a rest on the group wouldn't make sense. Hence why you said it should be group.form.reset. That makes more sense now and actually clears my issue since I can just do a type assert for an empty object.
cloudy-cyan•2mo ago
not quite comparable to withForm. The values do actually need to be there so that the group can map from itself to the form and vice versa
absent-sapphireOP•2mo ago
So it doesn't pull it from the form parent data? Hmmm ok...well then back to the drawing board
cloudy-cyan•2mo ago
it pulls from both. Example:
absent-sapphireOP•2mo ago
Right, but that's based on the type for the defaultValues, right? What I mean is, the group default values could be {} as GroupType in the withFieldGroup function to define the values that it uses or am I misunderstanding?
cloudy-cyan•2mo ago
to determine that
nested
has to be mappable to a nested.a
, the group needs to know that a
exists
which it cannot do with an empty record