I'm trying to get react-easy-crop to work. The code successfully shows a button (Upload file) which triggers a screen to select a picture. Once the picture is taken, a new modal appears and this modal shows a round cropping overlay on top of the picture. The goals is to resize the picture and save it in croppedImage so I can store it to the backend.
The issue at hand is that 'onCropComplete' is called over and over again. I can see that by putting print statements. Removing the line
setCroppedAreaPixels(croppedAreaPixels);
resolves the issue although it is unclear why this happens.
Can anyone help me as to why this function keeps getting called? My code is attached below.
// The `Cropper` component is on the global object as `ReactEasyCrop`. Assign to `Cropper` so we can use it like a ES6 module
window.Cropper = window.ReactEasyCrop;
const { createRoot } = ReactDOM;
const { StrictMode, useEffect, useState, useRef, useCallback } = React;
// Mock `useAxios`
function useAxios() {}
// Mock `useParams`
function useParams() {
return {
id: 1,
}
}
// Mock `getCroppedImg`
function getCroppedImg() {}
const UserDetail = () => {
const { id } = useParams();
const [res, setRes] = useState("");
const [dateJoined, setDateJoined] = useState("");
const [avatar, setAvatar] = useState("");
const [modalState, setModalState] = useState(false);
const api = useAxios();
const inputRef = useRef(null);
const [selectedImage, setSelectedImage] = useState(null);
const [crop, setCrop] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
const [croppedImage, setCroppedImage] = useState(null);
const showHideClassName = modalState
? "modal display-block"
: "modal display-none";
useEffect(() => {
console.log("useeffect!");
const fetchData = () => {
// make unrelated API call to backend
};
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
const handleFileChange = (event) => {
// works - saves file to selected image and triggers cropping modal
console.log(event.target.files[0].name);
setSelectedImage(event.target.files[0]);
showModal();
};
const handleFileUpload = (event) => {
// function not yet called because problem in onCropComplete
showCroppedImage();
const sendData = () => {
// API call to POST newly cropped image
};
sendData();
hideModal();
};
const showModal = () => {
setModalState(true);
};
const hideModal = () => {
setModalState(false);
};
const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => {
//problematic function, is called infinitely
setCroppedAreaPixels(croppedAreaPixels);
console.log(croppedAreaPixels);
}, []);
const showCroppedImage = useCallback(() => {
//code not working yet, first onCropComplete should work correctly
try {
// Converted async/await function to promise as Stack Snippets does not support async/await when transpiling with Babel as Babel version is too old. See: https://meta.stackoverflow.com/questions/386097/why-wont-this-snippet-with-async-await-work-here-on-stack-overflow-snippet-edit
const croppedImage = getCroppedImg(
selectedImage,
croppedAreaPixels,
0
).then(() => {
console.log("donee", { croppedImage });
setCroppedImage(croppedImage);
});
} catch (e) {
console.error(e);
}
}, [selectedImage, croppedAreaPixels]);
return (
<div className="parent">
<div className="userdetail">
<div className="profilebanner">
<div className="profilepicture">
<img src={avatar} alt="" className="avatar" />
<input
ref={inputRef}
onChange={handleFileChange}
type="file"
style={{ display: "none" }}
/>
<button onClick={() => inputRef.current.click()}>
Upload File
</button>
<div className={showHideClassName}>
<section className="modal-main">
<p align="center">Modal</p>
<div className="cropper">
{selectedImage && (
<Cropper
image={URL.createObjectURL(selectedImage)}
crop={crop}
aspect={5 / 5}
zoom={zoom}
zoomSpeed={4}
maxZoom={3}
zoomWithScroll={true}
showGrid={true}
cropShape="round"
onCropChange={setCrop}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
/>
)}
</div>
<button onClick={handleFileUpload}>Confirm!</button>
<button type="button" onClick={hideModal}>
Close
</button>
</section>
</div>
</div>
</div>
</div>
</div>
);
};
const root = createRoot(document.getElementById("root"));
root.render(<StrictMode><UserDetail /></StrictMode>);
<script crossorigin defer src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin defer src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script crossorigin defer src="https://unpkg.com/tslib@1.9.1/tslib.js"></script>
<!-- tslib assigns its functions to the global object (e.g. `window.__assign`) however React Easy Crop expects it on the tslib object (e.g. `window.tslib.__assign`). This is hacky but allows React Easy Crop to find tslib functions on `window.tslib` -->
<script type="module">window.tslib = window</script>
<script crossorigin defer src="https://unpkg.com/react-easy-crop@3.3.2/umd/react-easy-crop.js"></script>
<div id="root"></div>
Currently your code works like this:
selectedImage
state to the file. This causes the component to re-render.
const handleFileChange = (event) => { // works - saves file to selected image and triggers cropping modal console.log(event.target.files[0].name); setSelectedImage(event.target.files[0]); showModal(); };
image
prop of the Cropper
component.
return ( // ... { selectedImage && ( <Cropper image={URL.createObjectURL(selectedImage)} // ... /> } );
This is a problem because the following happens during render:
URL.createObjectURL
creates a new unique URL.Cropper
re-renders because of the new URL passed to image
.onCropComplete
is called because a default crop is applied.UserDetail
component re-renders because onCropComplete
updates state. This returns us to step 1 and causes an infinite loop.The key thing to note is URL.createObjectURL
is not deterministic. It produces a new result each time even with the same file, according to the algorithm in its specification. See step 9, which generates a UUID:
To generate a new blob URL, run the following steps:
- Let result be the empty string.
- Append the string "
blob:
" to result.- Let settings be the current settings object
- Let origin be settings’s origin.
- Let serialized be the ASCII serialization of origin.
- If serialized is "
null
", set it to an implementation-defined value.- Append serialized to result.
- Append U+0024 SOLIDUS (/) to result.
- Generate a UUID [RFC4122] as a string and append it to result.
- Return result.
As a result, instead of calling URL.createObjectURL
inside the image
prop for the Cropper
component, call it before you set the selectedImage
state. Demo below:
// The `Cropper` component is on the global object as `ReactEasyCrop`. Assign to `Cropper` so we can use it like a ES6 module
window.Cropper = window.ReactEasyCrop;
const { createRoot } = ReactDOM;
const { StrictMode, useEffect, useState, useRef, useCallback } = React;
// Mock `useAxios`
function useAxios() {}
// Mock `useParams`
function useParams() {
return {
id: 1,
}
}
// Mock `getCroppedImg`
function getCroppedImg() {}
const UserDetail = () => {
const { id } = useParams();
const [res, setRes] = useState("");
const [dateJoined, setDateJoined] = useState("");
const [avatar, setAvatar] = useState("");
const [modalState, setModalState] = useState(false);
const api = useAxios();
const inputRef = useRef(null);
const [selectedImage, setSelectedImage] = useState(null);
const [crop, setCrop] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
const [croppedImage, setCroppedImage] = useState(null);
const showHideClassName = modalState
? "modal display-block"
: "modal display-none";
useEffect(() => {
console.log("useeffect!");
const fetchData = () => {
// make unrelated API call to backend
};
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
const handleFileChange = (event) => {
// works - saves file to selected image and triggers cropping modal
console.log(event.target.files[0].name);
setSelectedImage(URL.createObjectURL(event.target.files[0]));
showModal();
};
const handleFileUpload = (event) => {
// function not yet called because problem in onCropComplete
showCroppedImage();
const sendData = () => {
// API call to POST newly cropped image
};
sendData();
hideModal();
};
const showModal = () => {
setModalState(true);
};
const hideModal = () => {
setModalState(false);
};
const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => {
//problematic function, is called infinitely
setCroppedAreaPixels(croppedAreaPixels);
console.log(croppedAreaPixels);
}, []);
const showCroppedImage = useCallback(() => {
//code not working yet, first onCropComplete should work correctly
try {
// Converted async/await function to promise as Stack Snippets does not support async/await when transpiling with Babel as Babel version is too old. See: https://meta.stackoverflow.com/questions/386097/why-wont-this-snippet-with-async-await-work-here-on-stack-overflow-snippet-edit
const croppedImage = getCroppedImg(
selectedImage,
croppedAreaPixels,
0
).then(() => {
console.log("donee", { croppedImage });
setCroppedImage(croppedImage);
});
} catch (e) {
console.error(e);
}
}, [selectedImage, croppedAreaPixels]);
return (
<div className="parent">
<div className="userdetail">
<div className="profilebanner">
<div className="profilepicture">
<img src={avatar} alt="" className="avatar" />
<input
ref={inputRef}
onChange={handleFileChange}
type="file"
style={{ display: "none" }}
/>
<button onClick={() => inputRef.current.click()}>
Upload File
</button>
<div className={showHideClassName}>
<section className="modal-main">
<p align="center">Modal</p>
<div className="cropper">
{selectedImage && (
<Cropper
image={selectedImage}
crop={crop}
aspect={5 / 5}
zoom={zoom}
zoomSpeed={4}
maxZoom={3}
zoomWithScroll={true}
showGrid={true}
cropShape="round"
onCropChange={setCrop}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
/>
)}
</div>
<button onClick={handleFileUpload}>Confirm!</button>
<button type="button" onClick={hideModal}>
Close
</button>
</section>
</div>
</div>
</div>
</div>
</div>
);
};
const root = createRoot(document.getElementById("root"));
root.render(<StrictMode><UserDetail /></StrictMode>);
<script crossorigin defer src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin defer src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script crossorigin defer src="https://unpkg.com/tslib@1.9.1/tslib.js"></script>
<!-- tslib assigns its functions to the global object (e.g. `window.__assign`) however React Easy Crop expects it on the tslib object (e.g. `window.tslib.__assign`). This is hacky but allows React Easy Crop to find tslib functions on `window.tslib` -->
<script type="module">window.tslib = window</script>
<script crossorigin defer src="https://unpkg.com/react-easy-crop@3.3.2/umd/react-easy-crop.js"></script>
<div id="root"></div>
It's also a good idea to release the object URL to aid memory management. From MDN:
Memory management
Each time you call
createObjectURL()
, a new object URL is created, even if you've already created one for the same object. Each of these must be released by callingURL.revokeObjectURL()
when you no longer need them.Browsers will release object URLs automatically when the document is unloaded; however, for optimal performance and memory usage, if there are safe times when you can explicitly unload them, you should do so.
I think you can do this inside the handleFileUpload
function.