"use client"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; const itemSchema = z.object({ name: z.string().min(1, "Name is required").max(100), quantity: z.coerce .number({ error: "Quantity must be a number" }) .int("Must be a whole number") .min(0, "Minimum 0") .max(10000, "Maximum 10000"), }); type ItemFormValues = z.input; type ItemSubmitValues = z.output; export function ItemForm() { const queryClient = useQueryClient(); const { register, handleSubmit, reset, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(itemSchema), defaultValues: { name: "", quantity: 1 }, }); const mutation = useMutation({ mutationFn: async (values: ItemSubmitValues) => { const res = await fetch("/api/items", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(values), }); if (!res.ok) { const err = await res.json().catch(() => null); throw new Error(err?.error ?? "Failed to create item"); } return res.json(); }, onSuccess: () => { toast.success("Item created"); reset(); queryClient.invalidateQueries({ queryKey: ["items"] }); }, onError: (err: Error) => { toast.error(err.message); }, }); return (
mutation.mutate(values))} className="flex flex-col gap-3 rounded-lg border p-4" noValidate >

Add item (RHF + zod + react-query)

{errors.name && {errors.name.message}}
{errors.quantity && ( {errors.quantity.message} )}
); }