{"id":11196,"date":"2024-09-04T08:47:08","date_gmt":"2024-09-04T08:47:08","guid":{"rendered":"https:\/\/www.bacancytechnology.com\/qanda\/?p=11196"},"modified":"2024-09-04T08:47:08","modified_gmt":"2024-09-04T08:47:08","slug":"handling-image-uploads-in-react-hook-form-with-zod","status":"publish","type":"post","link":"https:\/\/www.bacancytechnology.com\/qanda\/react\/handling-image-uploads-in-react-hook-form-with-zod","title":{"rendered":"React-hook-form, Zod What is the Recommended Way of Handling Image Uploads in a Form That Supports Both Adding and Editing?"},"content":{"rendered":"<p>Combined Approach with Unified Schema and Conditional Handling<\/p>\n<h3>Approach Overview:<\/h3>\n<ul>\n<li><strong>Single Schema:<\/strong> Use a single schema with a conditional image validation based on whether the form is in &#8220;add&#8221; or &#8220;edit&#8221; mode.<\/li>\n<li><strong>Unified Form Logic:<\/strong> Simplify the form logic by handling image previews within the form state, while keeping the option to handle image conversion (if needed).<\/li>\n<\/ul>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">const { data, isFetching } = useQuery(\r\nimport React from \"react\";\r\nimport { z } from \"zod\";\r\nimport { useForm, Controller } from \"react-hook-form\";\r\nimport { zodResolver } from \"@hookform\/resolvers\/zod\";\r\nimport { useEffect, useState } from \"react\";\r\n\r\nconst productFormSchema = z.object({\r\n  name: z.string().min(1, { message: \"Name is required\" }),\r\n image: z\r\n    .union([\r\n      z.instanceof(File, { message: \"Image is required\" }),\r\n      z.string().optional(), \/\/ Allow the existing image URL for editing mode\r\n    ])\r\n    .refine((value) =&gt; value instanceof File || typeof value === \"string\", {\r\n      message: \"Image is required\",\r\n    }),\r\n});\r\nexport type ProductFormValues = z.infer&lt;typeof productFormSchema&gt;;\r\ninterface ProductForm2Props { product?: Product; }\r\n\r\nexport const ProductForm1 = ({ product }: ProductForm2Props) =&gt; {\r\n  const isAddMode = !product;\r\n  const [imagePreview, setImagePreview] = useState&lt;string | null&gt;(\r\n    product?.image ?? null\r\n  );\r\n\r\n  const {\r\n    register,\r\n    handleSubmit,\r\n    control,\r\n    watch,\r\n    reset,\r\n    formState: { errors, isSubmitting, isDirty },\r\n  } = useForm&lt;ProductFormValues&gt;({\r\n    resolver: zodResolver(productFormSchema),\r\n    defaultValues: {\r\n      name: product?.name ?? \"\",\r\n      image: product?.image ?? \"\", \/\/ Use the existing image URL for editing mode\r\n    },\r\n  });\r\n\r\n  const image = watch(\"image\");\r\n  useEffect(() =&gt; {\r\n    if (image instanceof File) {\r\n      const imageUrl = URL.createObjectURL(image);\r\n      setImagePreview(imageUrl);\r\n return () =&gt; URL.revokeObjectURL(imageUrl);\r\n    }\r\n    if (typeof image === \"string\") {\r\n      setImagePreview(image);\r\n    }\r\n  }, [image]);\r\n\r\n  const onSubmitHandler = async (data: ProductFormValues) =&gt; {\r\n    console.log(data);\r\n\r\n    let imageUrl: string | undefined;\r\n    if (data.image instanceof File) {\r\n      \/\/ build FormData for uploading image\r\n      const formData = new FormData();\r\n      formData.append(\"file\", data.image);\r\n\r\n      \/\/ mock upload image to server to get image url\r\n      imageUrl = await new Promise&lt;string&gt;((resolve) =&gt; {\r\n        setTimeout(() =&gt; {\r\n          resolve(\"https:\/\/via.placeholder.com\/150\");\r\n        }, 1000);\r\n      });\r\n    } else {\r\n      imageUrl = data.image; \/\/ Use the existing image URL for updating mode\r\n    }\r\n    if (isAddMode) {\r\n      \/\/ create product\r\n      console.log({ ...data, image: imageUrl! });\r\n    } else {\r\n      \/\/ update product\r\n      console.log({ id: product!.id, ...data, image: imageUrl });\r\n    }\r\n    reset();\r\n  };\r\n  return (\r\n    &lt;form onSubmit={handleSubmit(onSubmitHandler)}&gt;\r\n      &lt;input {...register(\"name\")} \/&gt;\r\n    {errors.name &amp;&amp; &lt;span&gt;{errors.name.message}&lt;\/span&gt;}\r\n      &lt;Controller\r\n        name=\"image\"\r\n        control={control}\r\n        render={({ field: { ref, name, onBlur, onChange } }) =&gt; (\r\n          &lt;input\r\n            type=\"file\"\r\n            ref={ref}\r\n            name={name}\r\n            onBlur={onBlur}\r\n            onChange={(e) =&gt; {\r\n              const file = e.target.files?.[0];\r\n              onChange(file ? file : imagePreview); \/\/ Keep the existing image in edit mode\r\n              setImagePreview(\r\n                file ? URL.createObjectURL(file) : product?.image ?? null\r\n              );\r\n            }}\r\n          \/&gt;\r\n        )}\r\n      \/&gt;\r\n      {imagePreview &amp;&amp; &lt;img src={imagePreview} alt=\"preview\" \/&gt;}\r\n      {errors.image &amp;&amp; &lt;span&gt;{errors.image.message}&lt;\/span&gt;}\r\n\r\n      &lt;button type=\"submit\" disabled={(!isAddMode &amp;&amp; !isDirty) || isSubmitting}&gt;\r\n        {isSubmitting ? \"Submitting...\" : \"Submit\"}\r\n      &lt;\/button&gt;\r\n    &lt;\/form&gt;\r\n  );\r\n};\r\n<\/pre>\n<p>This approach should simplify your codebase while retaining flexibility for future enhancements.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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 &#8220;add&#8221; or &#8220;edit&#8221; mode. Unified Form Logic: Simplify the form logic by handling image previews within the form state, while keeping the option to handle image conversion [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":11197,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-11196","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-react"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/posts\/11196"}],"collection":[{"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/comments?post=11196"}],"version-history":[{"count":2,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/posts\/11196\/revisions"}],"predecessor-version":[{"id":11199,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/posts\/11196\/revisions\/11199"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/media\/11197"}],"wp:attachment":[{"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/media?parent=11196"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/categories?post=11196"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/tags?post=11196"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}