如何避免react-easy-crop中onCropComplete触发无限循环问题
问题分析:react-easy-crop中onCropComplete无限调用的原因
问题描述
我正在用react-easy-crop实现图片裁剪功能,已完成「上传文件」按钮、图片选择弹窗和模态框内圆形裁剪遮罩的功能,目标是裁剪图片并保存到croppedImage状态,用于后续上传至后端。
目前遇到的问题是onCropComplete函数被反复调用,通过打印日志可观察到该现象。移除代码中的setCroppedAreaPixels(croppedAreaPixels);行后,问题得以解决,但不清楚具体原因。
相关代码如下:
JS代码
// 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>);
HTML代码
<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>
原因分析
核心是状态更新触发组件重渲染,导致Cropper组件重新计算裁剪区域,进而再次调用onCropComplete,形成无限循环:
- 调用
setCroppedAreaPixels更新状态时,UserDetail组件会触发重渲染。 - 组件重渲染后,
Cropper组件会重新执行内部的裁剪区域计算逻辑(组件重新渲染时会触发这类依赖视图的计算)。 - 计算完成后,
Cropper会再次调用onCropComplete,而该函数又会更新croppedAreaPixels状态,循环往复导致函数被无限调用。
解决方案
方案1:用Ref存储裁剪区域,避免状态更新触发重渲染
不需要实时将裁剪区域保存到状态,而是用useRef存储,仅在用户确认裁剪时再使用该数据:
// 将croppedAreaPixels从useState改为useRef const croppedAreaPixelsRef = useRef(null); const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => { // 用ref存储,不会触发组件重渲染 croppedAreaPixelsRef.current = croppedAreaPixels; console.log(croppedAreaPixels); }, []); const showCroppedImage = useCallback(() => { try { getCroppedImg( selectedImage, croppedAreaPixelsRef.current, 0 ).then((result) => { console.log("donee", { result }); setCroppedImage(result); }); } catch (e) { console.error(e); } }, [selectedImage]);
方案2:限制状态更新的触发条件
如果确实需要实时保存裁剪区域到状态,可以对比新旧值,仅当数据变化时才更新:
const croppedAreaPixelsRef = useRef(null); const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => { // 对比新旧值,仅不同时才更新状态 if (!croppedAreaPixelsRef.current || JSON.stringify(croppedAreaPixelsRef.current) !== JSON.stringify(croppedAreaPixels)) { croppedAreaPixelsRef.current = croppedAreaPixels; setCroppedAreaPixels(croppedAreaPixels); } console.log(croppedAreaPixels); }, []);
内容的提问来源于stack exchange,提问作者MatisDS
相关产品推荐
相关产品推荐

