I recently dabbled in react forms and got React Hook Forms setup to be used with my Zod schemas, it all works perfectly... that is until I reset the form. The reset causes the formState (to my knowledge) go stale as validation messages show that the title input is empty, but inspecting its value property through code and looking at the display of typed characters on the screen proves otherwise.
I tried multiple stuff and went through the React Hook Form docs, but tbh the docs were utterly useless as I couldn't make heads or tails of any of it. Interestingly tho, using watcher to re-render the react component causes all of it to work so I suspect its a synchronization issue, but I'm still starting out so I don't know for sure...
Schemas:
const PLAIN_DATETIME_REGEX =
/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,9})?$/;
const DISPLAY_TIME_REGEX = /\bMoments\b|[hm]$/;
export const taskSchema = z.object({
id: z.uuid().default(() => crypto.randomUUID()),
status: z.literal(["complete", "incomplete"]).default("incomplete"),
title: z
.string()
.trim()
.min(1, "Title cannot be empty")
.max(100, "Title cannot exceed 100 characters"),
description: z.preprocess(
(raw: string) => raw?.trim() || undefined,
z
.string()
.max(2000, "Description cannot exceed 2000 characters")
.optional(),
),
createdAt: z.iso.datetime().default(() => Temporal.Now.instant().toString()),
deadline: z.preprocess(
(raw: string) => raw?.trim() || undefined,
z
.string()
.regex(
PLAIN_DATETIME_REGEX,
"Deadline must be a valid ISO 8601 plain date and time",
)
.optional(),
),
priority: z.literal(["low", "medium", "high"]),
});
export const taskFormSchema = taskSchema.omit({
id: true,
status: true,
createdAt: true,
});
export const taskViewSchema = taskSchema.transform(
({ description, createdAt, deadline, ...rest }) => ({
...rest,
description: description,
createdAt: formatDateTimeSince(createdAt),
deadline: formatDateTimeUntil(deadline),
}),
);
export type Task = z.infer<typeof taskSchema>;
export type TaskForm = z.infer<typeof taskFormSchema>;
export type TaskView = z.infer<typeof taskViewSchema>;
Form Component:
export default function TaskSaver() {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<TaskForm>({
resolver: zodResolver(taskFormSchema),
mode: "onChange",
defaultValues: {
title: "",
description: "",
deadline: "",
priority: "low",
},
});
const addTask = useTasksStore((state) => state.addTask);
const id = useId();
const onSubmit: SubmitHandler<TaskForm> = (data) => {
const result = taskSchema.safeParse(data);
if (!result.success) {
console.error(result.error);
return;
}
addTask(result.data);
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)} className={styles.root}>
<div className={styles.group}>
<label htmlFor={`title-${id}`} className="sr-only">
Title
</label>
<input
type="text"
{...register("title")}
id={`title-${id}`}
className={clsx(styles.input, styles.inline)}
placeholder="Title"
/>
{errors.title && <p className={styles.error}>{errors.title.message}</p>}
</div>
<div className={styles.group}>
<label htmlFor={`description-${id}`} className="sr-only">
Description
</label>
<textarea
{...register("description")}
id={`description-${id}`}
className={clsx(styles.input, styles.multiline)}
placeholder="Description"
></textarea>
{errors.description && (
<p className={styles.error}>{errors.description.message}</p>
)}
</div>
{/* Needs to be worked on: below */}
<div className={styles.group}>
<label htmlFor={`deadline-${id}`} className="sr-only">
Deadline
</label>
<input
{...register("deadline")}
id={`deadline-${id}`}
className={clsx(styles.input, styles.inline)}
placeholder="Deadline"
/>
{errors.deadline && (
<p className={styles.error}>{errors.deadline.message}</p>
)}
</div>
<div className={styles.group}>
<label htmlFor={`priority-${id}`} className="sr-only">
Priority
</label>
<select
{...register("priority")}
id={`priority-${id}`}
className={clsx(styles.input, styles.dropdown)}
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div className={styles.group}>
<button type="button" className={clsx(styles.button, styles.reject)}>
Cancel
</button>
<button type="submit" className={clsx(styles.button, styles.accept)}>
Save
</button>
</div>
</form>
);
}