You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免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,形成无限循环:

  1. 调用setCroppedAreaPixels更新状态时,UserDetail组件会触发重渲染。
  2. 组件重渲染后,Cropper组件会重新执行内部的裁剪区域计算逻辑(组件重新渲染时会触发这类依赖视图的计算)。
  3. 计算完成后,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 06:13:09