T
TanStack6mo ago
sunny-green

Handling defaultValues for a required number field

I know this is more of a general React issue than a TF-specific one, but I didn’t run into it with RHF since they don’t use controlled components by default. I have a required amount field that should be a number. If I use z.number(), I run into the default value issue—I don’t want 0 (bad UX), "" isn’t assignable to number, and undefined causes React’s controlled/uncontrolled warning. Would it be better to keep it as a string (""), then parse it before submission? Or is there a cleaner approach?
22 Replies
genetic-orange
genetic-orange6mo ago
z.coerce.number() can cause NaN if the string is empty, so that isn‘t really cleaner. Is there a reason why 0 shouldn‘t be the default value if you want the field to be required?
sunny-green
sunny-greenOP6mo ago
Yes exactly, I have a lot of amount inputs in my app, setting the default value as 0 requires the user to delete everytime before entering the new value, and it is not ideal for my UX
fair-rose
fair-rose6mo ago
@Mohamed Yahye El Joud Try the following: Add this reusable piece to your codebase:
export const nonEmptyStringToNumberSchema = (numberSchema: z.ZodNumber) =>
z
.string()
.nonempty({
message: "Required",
})
.refine((value) => !isNaN(Number(value)), {
message: "Must be a number",
})
.pipe(numberSchema);
export const nonEmptyStringToNumberSchema = (numberSchema: z.ZodNumber) =>
z
.string()
.nonempty({
message: "Required",
})
.refine((value) => !isNaN(Number(value)), {
message: "Must be a number",
})
.pipe(numberSchema);
Now to use it:
creditLimit: nonEmptyStringToNumberSchema(
z.coerce.number().min(0, {
message: "Credit limit must be greater than 0",
}),
),
creditLimit: nonEmptyStringToNumberSchema(
z.coerce.number().min(0, {
message: "Credit limit must be greater than 0",
}),
),
z.input will infer to string and z.output will infer to number! This is how we do it on our codebase. The main annoying part is that you still need to coerce to number. There might be a way to have the base scehma coerce but I'm not sure how.
rival-black
rival-black6mo ago
Do you use this in a text or number input? Asking because I wanna do something like that but dont allow the user to write letters if its number input field
fair-rose
fair-rose6mo ago
I use it in an input with type text, however I use react-number-format to limit what the user can type.
conscious-sapphire
conscious-sapphire6mo ago
I have a similar use case where I have a number field that is not required and I don't want an initial value. Is there an elegant way to solve this? What I think I truly want is nullable fields but that doesn't seem to play nice with controlled inputs
fair-rose
fair-rose6mo ago
You want to set a number field initially to null?
conscious-sapphire
conscious-sapphire6mo ago
or undefined yes. It is an optional field I have the same issue with optional string fields as well. I don't want to save empty strings to my DB
fair-rose
fair-rose6mo ago
Got it! would something like this work:
z.number().nullable().refine(val => val != null { message: 'required' })
z.number().nullable().refine(val => val != null { message: 'required' })
z.input would infer to number | null (allowing you to initiate as null) z.output would infer to number
conscious-sapphire
conscious-sapphire6mo ago
but i don't want it to be required
genetic-orange
genetic-orange6mo ago
perhaps transform can turn empty trimmed strings into null and when setting the input‘s value, you fall back to ?? „“
conscious-sapphire
conscious-sapphire6mo ago
transform() doesn't do anything in validator schemas from what I've seen
genetic-orange
genetic-orange6mo ago
(mobile coding :Sadge: )
fair-rose
fair-rose6mo ago
So why is z.number().optional() not okay? Sorry not sure what I'm missing.
genetic-orange
genetic-orange6mo ago
I think the issue is that html controlled inputs shouldn‘t receive null so the question is how should that be handled
fair-rose
fair-rose6mo ago
Ahh true true, sorry since our team always initiates fields in forms as empty strings (except for like select) fields, we don't run into that issue. I'd agree with you Luca, if you MUST initiate with null, you'd have to cast the value prop ?? "". At least I'm not sure of another way now.
genetic-orange
genetic-orange6mo ago
number inputs are terrible in general to handle. On Firefox, I noticed that inputs of type number don‘t trigger onChange if the value is NaN
conscious-sapphire
conscious-sapphire6mo ago
Yeah I still can get by with all string inputs but that leads to other pain when trying to replace all the empty strings of optional fields with null
genetic-orange
genetic-orange6mo ago
what about doing that transform from empty string to null before passing it to handleChange?
conscious-sapphire
conscious-sapphire6mo ago
<Input
{...rest}
id={label}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value === '' ? undefined : e.target.value)}
/>
<Input
{...rest}
id={label}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value === '' ? undefined : e.target.value)}
/>
are you saying like this?
genetic-orange
genetic-orange6mo ago
yeah, but I haven‘t tested it. Of course it implies that the state value can be undefined, so a nullish coalescer to an empty string is also required
conscious-sapphire
conscious-sapphire6mo ago
i had to add | undefined here const field = useFieldContext<string | undefined>(); it works but i still get a Type 'string' is not assignable to type 'undefined' TS error for onSubmit unless i add as z.input<typeof FormSchema> to my defaultValues object the undefined default value works fine even without the handleChange change up above it just causes TS errors unless you type cast your defaultValues I'll have to revisit this with a fresh mind tomorrow, tried a ton of things and couldn't get a nice solution

Did you find this page helpful?