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:
const MySchema = z.object({
name: z.string(),
amount: z.number(),
});
const MySchema = z.object({
name: z.string(),
amount: z.number(),
});
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:
type FormData = {
name: string;
amount: number | "";
}
type FormData = {
name: string;
amount: number | "";
}
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
<input
// ...
{...register("someProp")}
onChange={(e) => onChange(transformEventTargetStringValueToActualValueOfWhateverTypeMyPropInTheSchemaHas(e.target.value))}
/>
<input
// ...
{...register("someProp")}
onChange={(e) => onChange(transformEventTargetStringValueToActualValueOfWhateverTypeMyPropInTheSchemaHas(e.target.value))}
/>
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
Brendonovich
Brendonovich•2y ago
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!
Brendonovich
Brendonovich•2y ago
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
Froxx
FroxxOP•2y ago
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
Brendonovich
Brendonovich•2y ago
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
stanisław
stanisław•2y ago
@Brendonovich Amazing post thanks! Definitely gonna add this into our work codebase Didn't even know RHF has its own provider ;p
Brendonovich
Brendonovich•2y ago
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 ...
stanisław
stanisław•2y ago
sure I will take a look, thanks
Froxx
FroxxOP•2y ago
@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?
Brendonovich
Brendonovich•2y ago
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?
stanisław
stanisław•2y ago
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?
Brendonovich
Brendonovich•2y ago
In Spacedrive we use mutateAsync, it's super handy
stanisław
stanisław•2y ago
// works
const onSubmit = async (data: FormValues) => {
try {
await mutateAsync(data);
} catch (err) {
// set errors in form or w/e
}
};

<Form {...form} onSubmit=(onSubmit) />
// works
const onSubmit = async (data: FormValues) => {
try {
await mutateAsync(data);
} catch (err) {
// set errors in form or w/e
}
};

<Form {...form} onSubmit=(onSubmit) />
// doens't work becuase mutation isn't async

const { mutate } = useMutation(...)

<Form {...form} onSubmit(mutate) />
// doens't work becuase mutation isn't async

const { mutate } = useMutation(...)

<Form {...form} onSubmit(mutate) />
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 ;/
Brendonovich
Brendonovich•2y ago
hmm, we actually require that handleSubmit is called manually for that reason. Might update my blog with that
Brendonovich
Brendonovich•2y ago
stanisław
stanisław•2y ago
Oh could I get a github link with some more code? @Brendonovich
Brendonovich
Brendonovich•2y ago
GitHub
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...
stanisław
stanisław•2y ago
Thank you! I think updating your blog post on this topic would be really helpful for people using mutations + your form
Brendonovich
Brendonovich•2y ago
Yeah i'll keep it in mind u should see how we trigger dialogs with button presses, i think it's pretty unique haha
stanisław
stanisław•2y ago
Hah thank you for this repository Actually the stack is pretty much the same as I am using rn Even Tauri and Radix UI
Froxx
FroxxOP•2y ago
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! froxx9Love
Brendonovich
Brendonovich•2y ago
Oh that's awesome! Glad I could help 😄
Froxx
FroxxOP•2y ago
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 layer
Brendonovich
Brendonovich•2y ago
Hmmm 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 nice
Froxx
FroxxOP•2y ago
It 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
Brendonovich
Brendonovich•2y ago
It works at runtime, but I'm pretty sure that valueAsNumber won't affect the TS types
Froxx
FroxxOP•2y ago
Well 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 directly
Brendonovich
Brendonovich•2y ago
True, ig I'm just not comfortable with z.number() succeeding purely because valueAsNumber exists somewhere else. Whatever gets the job done tho
Want results from more Discord servers?
Add your server