Combined Approach with Unified Schema and Conditional Handling
const { data, isFetching } = useQuery( import React from "react"; import { z } from "zod"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useState } from "react"; const productFormSchema = z.object({ name: z.string().min(1, { message: "Name is required" }), image: z .union([ z.instanceof(File, { message: "Image is required" }), z.string().optional(), // Allow the existing image URL for editing mode ]) .refine((value) => value instanceof File || typeof value === "string", { message: "Image is required", }), }); export type ProductFormValues = z.infer<typeof productFormSchema>; interface ProductForm2Props { product?: Product; } export const ProductForm1 = ({ product }: ProductForm2Props) => { const isAddMode = !product; const [imagePreview, setImagePreview] = useState<string | null>( product?.image ?? null ); const { register, handleSubmit, control, watch, reset, formState: { errors, isSubmitting, isDirty }, } = useForm<ProductFormValues>({ resolver: zodResolver(productFormSchema), defaultValues: { name: product?.name ?? "", image: product?.image ?? "", // Use the existing image URL for editing mode }, }); const image = watch("image"); useEffect(() => { if (image instanceof File) { const imageUrl = URL.createObjectURL(image); setImagePreview(imageUrl); return () => URL.revokeObjectURL(imageUrl); } if (typeof image === "string") { setImagePreview(image); } }, [image]); const onSubmitHandler = async (data: ProductFormValues) => { console.log(data); let imageUrl: string | undefined; if (data.image instanceof File) { // build FormData for uploading image const formData = new FormData(); formData.append("file", data.image); // mock upload image to server to get image url imageUrl = await new Promise<string>((resolve) => { setTimeout(() => { resolve("https://via.placeholder.com/150"); }, 1000); }); } else { imageUrl = data.image; // Use the existing image URL for updating mode } if (isAddMode) { // create product console.log({ ...data, image: imageUrl! }); } else { // update product console.log({ id: product!.id, ...data, image: imageUrl }); } reset(); }; return ( <form onSubmit={handleSubmit(onSubmitHandler)}> <input {...register("name")} /> {errors.name && <span>{errors.name.message}</span>} <Controller name="image" control={control} render={({ field: { ref, name, onBlur, onChange } }) => ( <input type="file" ref={ref} name={name} onBlur={onBlur} onChange={(e) => { const file = e.target.files?.[0]; onChange(file ? file : imagePreview); // Keep the existing image in edit mode setImagePreview( file ? URL.createObjectURL(file) : product?.image ?? null ); }} /> )} /> {imagePreview && <img src={imagePreview} alt="preview" />} {errors.image && <span>{errors.image.message}</span>} <button type="submit" disabled={(!isAddMode && !isDirty) || isSubmitting}> {isSubmitting ? "Submitting..." : "Submit"} </button> </form> ); };
This approach should simplify your codebase while retaining flexibility for future enhancements.