I’m creating a screenshot submission form, and I want to compress the image before uploading it to my server. I’ve got the process so it works, but whenever I test it the first time I try submitting it fails. Clicking the submit button a second time, without changing anything, without re-selecting the file, just simply clicking submit again, it succeeds as I would expect it to.
I’m using Next.js, with swr and react-toastify to handle data fetching/caching and user notification respectively.
This is my compression function.
const compressImage = imgData =>
new Promise((resolve, reject) => {
const img = document.createElement("img");
img.src = imgData;
console.log(img);
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const originalWidth = img.width;
const originalHeight = img.height;
console.log(originalWidth, originalHeight);
const baseWidth = 480;
const baseHeight = 270;
const canvasWidth = Math.min(baseWidth, ((originalWidth * baseHeight) / originalHeight) | 0);
const canvasHeight = Math.min(baseHeight, ((originalHeight * baseWidth) / originalWidth) | 0);
console.log(canvasWidth, canvasHeight);
canvas.width = Math.min(originalWidth, canvasWidth);
canvas.height = Math.min(originalHeight, canvasHeight);
console.log(canvas.width, canvas.height);
try {
context.drawImage(img, 0, 0, canvasWidth, canvasHeight);
} catch (err) {
return reject(err);
}
// Reduce quality
canvas.toBlob(blob => {
if (blob) resolve(blob);
else reject("No blob");
}, "image/jpeg");
});
I suspect the issue is either here with how I’m creating the img element, or somewhere before here and this is where it’s making itself obvious.
When I check the logs I see the first attempt shows me the src being set correctly, but a size of 0 0 which then propagates down, and my expectation is that when the canvas size is 0 there’s nothing to put in the blob at the end.
<img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD…">
0 0
0 0
0 0
The second submit gives be logs like I would expect to see.
<img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD…">
1920 1080
480 270
480 270
I’m a bit lost on what to look for or what to change. It feels like somehow there’s a react hook that’s using stale data, but then why would the img src be correct in the compression function.
I’ve created this form for submitting:
<Modal.Body>
{screenshot && (
<img
src={screenshot}
alt="Submitted Screenshot"
width="100%"
style={{ objectFit: "contain" }}
/>
)}
<form
id="screenshotForm"
action={async formData => {
setSubmitting(true);
const screenshotFile = formData.get("screenshot");
// Is this actually redundant?
const screenshotData = await new Promise(resolve => {
const fr = new FileReader();
fr.addEventListener("load", e => resolve(e.target.result));
fr.readAsDataURL(screenshotFile);
});
try {
const compressedData = await compressImage(screenshotData);
const blobUrl = URL.createObjectURL(compressedData);
setScreenshot(blobUrl);
const formData = new FormData();
formData.append("image", compressedData);
formData.append("beatmapId", selectedMap.id);
formData.append("mods", selectedMap.mods);
await toast.promise(
mutate("/api/db/player", () => uploadScreenshot(formData), {
optimisticData: oldData => {
const updatedMaplist = oldData.maps.current;
const index = updatedMaplist.findIndex(
m => m.id === selectedMap.id && m.mods === selectedMap.mods
);
updatedMaplist[index].screenshot = compressedData.arrayBuffer();
return {
...oldData,
maps: {
...oldData.maps,
current: updatedMaplist
}
};
},
populateCache: (result, oldData) => ({
...oldData,
maps: {
...oldData.maps,
current: result
}
})
}),
{
pending: "Uploading",
success: "Image uploaded",
error: "Unable to upload image"
}
);
} catch (err) {
toast.error("Unable to upload image");
console.error(err);
}
setSubmitting(false);
}}
>
<FormLabel htmlFor="imageUpload">Upload Screenshot</FormLabel>
<FormControl type="file" accept="image/*" id="imageUpload" name="screenshot" />
</form>
</Modal.Body>
<Modal.Footer>
<Button type="submit" form="screenshotForm" disabled={submitting}>
Submit {submitting && <Spinner size="sm" />}
</Button>
<Button onClick={() => setShowModal(false)}>Done</Button>
</Modal.Footer>
Both of these are in the page.js file, which is declared as “use client”;
uploadScreenshot is imported from ‘./actions’ which is “use server”;
Another possible symptom is that the spinner on the button shown by submitting is never visible. But if that’s a separate issue then I don’t care about it here.