3

I'm trying to build a form that can upload multiple images.
The images can be removed during the preview images.

For example, I upload the two images then, I try to click the remove button to remove one of two images that are working fine.

The problem is during I click the submit button suppose will post one image to API but there are still remaining two images post to API.
I'm using the React hook form, anyone knows what is the problems? Here is the project links https://codesandbox.io/s/boring-hermann-xrqbt

const methods = useFormContext();
const { control, watch, setValue, formState } = methods;
const [image, setImage] = useState([]);

function deleteFile(id) {
  const s = image.filter((item, index) => item.id !== id);
  setImage(s);
}

<div className="flex justify-center sm:justify-start flex-wrap -mx-16">
  <Controller
    name="images"
    control={control}
    render={({ field: { onChange, value } }) => (
      <label
        htmlFor="button-file"
        className={clsx(
          classes.productImageUpload,
          "flex items-center justify-center relative w-128 h-128 rounded-16 mx-12 mb-24 overflow-hidden cursor-pointer shadow hover:shadow-lg"
        )}
      >
        <input
          accept="image/*"
          className="hidden"
          id="button-file"
          type="file"
          onChange={async (e) => {
            function readFileAsync() {
              return new Promise((resolve, reject) => {
                const file = e.target.files[0];
                if (!file) {
                  return;
                }
                const reader = new FileReader();

                reader.onload = () => {
                  resolve({
                    id: FuseUtils.generateGUID(),
                    url: `data:${file.type};base64,${btoa(reader.result)}`,
                    type: "image",
                  });
                };

                reader.onerror = reject;

                reader.readAsBinaryString(file);
              });
            }

            const newImage = await readFileAsync();

            setImage([newImage, ...image]);
            onChange([newImage, ...image]);
          }}
        />
        <Icon fontSize="large" color="action">
          cloud_upload
        </Icon>
      </label>
    )}
  />
  <Controller
    name="featuredImageId"
    control={control}
    defaultValue=""
    render={({ field: { onChange, value } }) =>
      image.map((media) => (
        <div
          onClick={() => onChange(media.id)}
          onKeyDown={() => onChange(media.id)}
          role="button"
          tabIndex={0}
          className={clsx(
            classes.productImageItem,
            "flex items-center justify-center relative w-128 h-128 rounded-16 mx-12 mb-24 overflow-hidden cursor-pointer outline-none shadow hover:shadow-lg",
            media.id === value && "featured"
          )}
          key={media.id}
        >
          <Icon
            className={classes.productImageFeaturedStar}
            onClick={() => deleteFile(media.id)}
          >
            Delete
          </Icon>
          <img
            className="max-w-none w-auto h-full"
            src={media.url}
            alt="product"
          />
        </div>
      ))
    }
  />
</div>;

1 Answer 1

3
+50

Edit still-sea-ijvrk


You can't programmatically set the value of an input with type="file" for security reasons.

<input
  ...
  type="file"
  onChange={uploadimg}
  // remove the value prop
/>

In the readFileAsync function, you need to account for the multiple files that will come, so instead we pass one File to it.

You were concatenating the result of the FileReader to get a valid Base64 string, but you can just use FileReader#readAsDataURL. And that function will return the Base64 string for the image.

function readFileAsync(file) {
  ...
    reader.onload = () => {
      resolve({
        id: uuid(),
        url: reader.result,
        type: "image",
      });
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

And in the uploadimg function,

async function uploadimg(e) {
  const file = e.target.files[0];
  if (file) {
    setImage([...images, (await readFileAsync(file))]);
    setFiles([...files, file]);
  }
}

In the deleteFiles function, now we have to update the image and files states.

Because in the files state, the images aren't recorded by anything unique, So we use the insert position (from the uploadimg function) to match the files and remove the one to delete.

function deleteFile(id) {
  const imageIndex = images.findIndex(item => item.id === id);

  if (imageIndex > -1) {
    setImages(images.filter(item => item.id !== id));
    setFiles(files.filter((_, i) => i !== imageIndex));
  }
}

}


For the saving

There isn't a need for for the react-hook-form package, since the input with type="file"'s value cannot be set programmatically.

So if it's the files from the input you want to access

  • you can use a ref to access it
  • store the file in another state
  • use the Base64 strings you generated.

I used another state to hold the files in the CodeSandbox, but you can use any of the three options.

Sign up to request clarification or add additional context in comments.

12 Comments

Thanks for your explanation and solution. but when i click the save button still can't get the values
@sy523. Check the edit
The results is no problem. But I need use react hook form to do it because the upload is one field of the form , another field all use react hook form methods .
@sy523; I’ll use the setValue function provided from the react-hook-form to set the image files. And then you should be able to get the images using getValues
@sy523; Check the chat
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.