反应钩子形式,Zod。以支持添加和编辑的形式处理图像上传的推荐方法是什么?

mon*_*ype 6 file-upload reactjs react-hook-form zod

以支持添加和编辑产品的形式处理图像上传的推荐方法是什么?这是我当前的实现,有两个选项,有什么改进建议吗?

这是当图像转换为 File 对象并将其设置为表单中图像的默认值时的第一个选项:

const getFileFromUrl = async (url: string) => {
  const res = await fetch(url);
  const blob = await res.blob();
  return new File([blob], 'image', { type: blob.type });
};

const productForm1Schema = z.object({
  name: z.string().min(1, { message: 'Name is required' }),
  image: z
    .custom<File>((v) => v instanceof File, {
      message: 'Image is required',
    })
});

export type ProductForm1Values = z.infer<typeof productForm1Schema>;

interface ProductForm1Props {
  product?: Product;
}

export const ProductForm1 = ({ product }: ProductForm1Props) => {
  const isAddMode = !product;

  const {
    register,
    handleSubmit,
    control,
    watch,
    reset,
    formState: { errors, isSubmitting, isDirty },
  } = useForm<ProductForm1Values>({
    resolver: zodResolver(productForm1Schema),
    defaultValues: product
      ? async () => ({
          name: product.name,
          image: await getFileFromUrl(product.image),
        })
      : {
          name: '',
          image: undefined,
        },
  });

  const image = watch('image');
  const imagePreview = image ? URL.createObjectURL(image) : null;

  // revoke object URL to avoid memory leaks
  useEffect(() => {
    return () => {
      if (imagePreview) URL.revokeObjectURL(imagePreview);
    };
  }, [imagePreview]);

  const onSubmitHandler = async (data: ProductForm1Values) => {
    console.log(data);

    // build FormData for uploading image
    const formData = new FormData();
    formData.append('file', data.image);

    // mock upload image to server to get image url
    const imageUrl = await new Promise<string>((resolve) => {
      setTimeout(() => {
        resolve('https://via.placeholder.com/150');
      }, 1000);
    });

    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) => onChange(e.target.files?.[0])}
          />
        )}
      />
      {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>
  );
};
Run Code Online (Sandbox Code Playgroud)

或者

这是我创建两个用于创建和更新产品的架构时的第二个选项,并且在更新架构中图像是可选的:

const createProductSchema = z.object({
  name: z.string().min(1, { message: 'Name is required' }),
  image: z
    .custom<File>((v) => v instanceof File, {
      message: 'Image is required',
    })
});

const updateProductSchema = createProductSchema.extend({
  image: createProductSchema.shape.image.optional(),
});

export type ProductForm2Values =
  | z.infer<typeof createProductSchema>
  | z.infer<typeof updateProductSchema>;

interface ProductForm2Props {
  product?: Product;
}

export const ProductForm2 = ({ product }: ProductForm2Props) => {
  const [imagePreview, setImagePreview] = useState<string | null>(
    product ? product.image : null,
  );

  const isAddMode = !product;

  const {
    register,
    handleSubmit,
    control,
    reset,
    formState: { errors, isSubmitting, isDirty },
  } = useForm<ProductForm2Values>({
    resolver: zodResolver(
      isAddMode ? createProductSchema : updateProductSchema,
    ),
    defaultValues: {
      name: product?.name ?? '',
      image: undefined,
    },
  });

  // revoke object URL to avoid memory leaks
  useEffect(() => {
    return () => {
      if (imagePreview) URL.revokeObjectURL(imagePreview);
    };
  }, [imagePreview]);

  const onSubmitHandler = async (data: ProductForm2Values) => {
    console.log(data);

    let imageUrl: string | undefined;
    if (data.image) {
      // 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);
      });
    }

    if (isAddMode) {
      // create product
      console.log({ ...data, image: imageUrl! });
    } else {
      // update product
      console.log({ id: product!.id, ...data, image: imageUrl });
    }

    reset();
    setImagePreview(product?.image ?? null);
  };

  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(e.target.files?.[0]);
              setImagePreview(file ? URL.createObjectURL(file) : 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>
  );
};
Run Code Online (Sandbox Code Playgroud)