Combined Approach with Unified Schema and Conditional Handling

Approach Overview:

  • Single Schema: Use a single schema with a conditional image validation based on whether the form is in “add” or “edit” mode.
  • Unified Form Logic: Simplify the form logic by handling image previews within the form state, while keeping the option to handle image conversion (if needed).
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.

Support On Demand!

ReactJS