Using undefined for empty input field in react-hook-form
First off: I feel I'm thinking way too complicated here, but can't wrap my brain around it, so here we are:
Let's say I have a schema:
I want to build a form with an input type "text" for
name
, and an input type "number" for amount
.
Do I need to create an extra "form type" of that schema to represent the data within the form before validation? So something like:
Imo this would make things really complicated since I would need to somewhere (where even?) map my form data to the actual schema data (e.g. amount: formData.amount === "" ? undefined : parseInt(formData.amount),
. So yeah, I would like to prevent things like that.
This is why I thought doing fancy things in my FormInput component like
once would be a good idea. But this also adds undefined
as a possible value for the input which React doesn't like since changing a component's value between undefined and a defined value makes it switch between being controlled / uncontrolled.
Like I said, I feel I'm thinking way too complicated here. Isn't there an easy solution to handle form input fields' data before validation?
I really don't want to add an additional FormData type for all of my schemas since I'm building lots and lots of forms.27 Replies
Not to be that guy but I think you'll like this blog post of mine: https://www.brendonovich.dev/blog/the-ultimate-form-abstraction
Brendonovich
The Ultimate Form Abstraction
Typesafe forms in React? Sounds like a job for React Hook Form and Zod!
tldr react hook form has support for a zod resolver that can do transformations for you, and if you combine some generics and custom hooks you can get fully typesafe form fields
Thanks "that guy", I don't mind ð
I'll have a look at that for a moment
I read a bit into it, and I already found a couple of things I can improve in my current setup. So thanks until here at very least ð
I don't know if my actual problem will also be solved by it, but I'll look into it again later on, when I have more time
In the specific case of an amount not being specified, if you use the
required
prop you can enforce that a value will always be provided
And with z.coerce
zod will transform the string to a number for you@Brendonovich Amazing post thanks!
Definitely gonna add this into our work codebase
Didn't even know RHF has its own provider ;p
If you want to see the approach I described in a big codebase, I implemented it in Spacedrive in this PR: https://github.com/spacedriveapp/spacedrive/pull/515
I didn't do the error stuff but the rest of it is there
GitHub
Begin form abstraction by Brendonovich · Pull Request #515 · spaced...
Form management right now is a fragmented nightmare, this PR addresses some of that using tips from my blog post.
useZodForm replaces useForm and requires defining a zod schema
Form automatically ...
sure I will take a look, thanks
@Brendonovich Alright, I got some time now, finished the whole thing and definitely can improve my form a bit, but mainly it helped be point a finger on the actual problem I'm having:
I build an app where I have different domains (product, campaign, user, etc.), and all of these domains work pretty much CRUD based, so the users can create, read, update and sometimes delete entities of those domains.
For this I create a specific form component for each of my domains that I call from 2 pages:
/domain/create
and /domain/{id}
.
If I only had my form in a "create" state, your guide would pretty much care about everything. But since I also need to edit existing entities of my domain, I need to be able to add initial values to my form fields, and have onChange listeners, since I also need to hold 2 states of my entity: The one currently persisted, and the one having the current states of all of my form fields.
Or am I still thinking too complex?In Spacedrive we just fetch existing data with
useQuery
, and pass it to the form's initialValues
. The form holds the updated state, the useQuery
holds the existing state. Do you need more control than that?Hey @Brendonovich I also have one question related to Tanstack Query. Have you encountered a problem that when using
mutate
from useMutation
fieldset doesn't become disabled? That's probably becuase mutate
isn't an async function and there are 2 potential solutions that I've found. Either use mutateAsync
or pass isLoading
from mutation to the <Form/>
component and then to fieldset
instead of form.formState.isSubmitting
What would you choose? Which solution is better in your opinion?In Spacedrive we use
mutateAsync
, it's super handy
so mutateAsync is the way to go there
Okey
The only problem is that we have to add extra types inferred from schemas to onSubmit ;/
hmm, we actually require that
handleSubmit
is called manually for that reason. Might update my blog with thatOh could I get a github link with some more code? @Brendonovich
yeah sure, our
Dialog
is a form + modal joined together https://github.com/spacedriveapp/spacedrive/blob/ef9fe5b75faaeff65b8bc93eae0b0ba73bc226a9/packages/interface/src/components/dialog/CreateLibraryDialog.tsxGitHub
spacedrive/CreateLibraryDialog.tsx at ef9fe5b75faaeff65b8bc93eae0b0...
Spacedrive is an open source cross-platform file explorer, powered by a virtual distributed filesystem written in Rust. - spacedrive/CreateLibraryDialog.tsx at ef9fe5b75faaeff65b8bc93eae0b0ba73bc22...
Thank you!
I think updating your blog post on this topic would be really helpful for people using mutations + your form
Yeah i'll keep it in mind
u should see how we trigger dialogs with button presses, i think it's pretty unique haha
Hah thank you for this repository
Actually the stack is pretty much the same as I am using rn
Even Tauri and Radix UI
Yes, I (again) thought too complex. I didn't really understand the form state and the whole
useFormContext
thing of react hook form before.
After fiddling around with it a little more I got a really nice and clean solution for my use case, even for nested forms which I had major problems with before.
Your guide helped be a lot with this. Thank you very, very much! Oh that's awesome! Glad I could help ð
Btw, in regards of Zod schemas I preferred not using
coerce
since I liked using my original model definition schema instead of a separate "form schema", and wanted to keep it al clean as possible.
Instead I used valueAsNumber: type === "number",
as a parameter for .register(name, {...})
to enforce the type before the schema even got involved.
That way I can simply use my core schema, maybe omit some props I don't want to handle in the form, and don't need another layerHmmm I see, personally I'm not a fan bc an input's
value
will always be typed as string
, even if valueAsNumber
is used, but I get why it can be niceIt does? Hitting submit at least validates the form fields' data correctly against my schema without throwing errors, so in onSubmit I get my validated data with numeric values
It works at runtime, but I'm pretty sure that
valueAsNumber
won't affect the TS typesWell in the form state TS knows it's a number since I pass my type to
useFormContext
, so getValues("myNumericValue") returns type number, and that's all that matters since that's the way I access the form's data, right?
I dont't need to access the input's value directlyTrue, ig I'm just not comfortable with
z.number()
succeeding purely because valueAsNumber
exists somewhere else. Whatever gets the job done tho