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

ReactJS图片重复下载问题及对应组件实现代码示例

React 图片重复下载问题修复

问题根因

  • 所有useEffect未指定依赖数组,组件每次重渲染都会重复执行副作用逻辑,引发额外的重渲染循环
  • 父组件的onDownloadFile下载方法未做缓存,每次父组件重渲染都会生成新的函数实例,导致子组件接收的props变化触发不必要重渲染,可能出现重复事件绑定
  • 下载逻辑未加并发锁,同一时间可多次触发下载请求,用户快速点击下载按钮会重复下载同一文件
  • 直接在useEffect回调中使用async函数,不符合React useEffect使用规范,可能引发内存泄漏问题

修复后代码

ShowFormsList.jsx

import React, { useEffect, useContext, useCallback } from "react";
import PropTypes from "prop-types";
import {
  Subheading,
  Accordion,
  AccordionItem,
} from "@contentful/forma-36-react-components";
import Jarvis from "../../utils/Jarvis";
import LoadingContext from "../../context/LoadingContext";
import ShowImgList from "./ShowImgList";

const ShowFormsList = ({ sdk }) => {
  const [view, setView] = React.useState(null);
  // 新增下载锁状态
  const [isDownloading, setIsDownloading] = React.useState(false);
  const jarvisSdk = new Jarvis(sdk);
  const { showLoading, hideLoading } = useContext(LoadingContext);
  const fieldType = (sdk && sdk.field.id) || "";

  const autoScroll = useCallback(() => {
    sdk.window.startAutoResizer();
  }, [sdk]);

  // 新增依赖数组,仅组件挂载时执行一次
  useEffect(() => {
    sdk.window.startAutoResizer();
  }, [sdk]);

  const getFormData = useCallback(async () => {
    showLoading();
    const formData = await jarvisSdk.getFormData(sdk.ids.entry);
    const formArr = 
      formData?.length ? formData.map((form) => {
        const formObj = {
          formName: form.formName,
          formId: form.id,
          formDescription: form.description,
        };
        const imgArr = form.formImages?.length ? form.formImages.map((img) => {
          return {
            imageName: img.imageName,
            type: img.type,
            extension: img.imageName.split(".")[1],
            id: img.id,
          };
        }) : [];
        formObj.images = imgArr;
        return formObj;
      }) : [];
    if (formArr.length) {
      setView(formArr);
    }
    hideLoading();
  }, [jarvisSdk, sdk, showLoading, hideLoading]);

  // 修正useEffect async写法,新增依赖数组
  useEffect(() => {
    getFormData();
  }, [getFormData]);

  // 用useCallback缓存下载方法,新增下载锁避免重复触发
  const onDownloadFile = useCallback(async (id) => {
    if (isDownloading) return;
    setIsDownloading(true);
    showLoading();
    try {
      const downloadedFile = await jarvisSdk.downloadFormFile(id, fieldType);
      const contentDispositionHeader = downloadedFile.responseObj.request
        .getResponseHeader("Content-disposition")
        .split("filename=");
      const fileName = contentDispositionHeader[contentDispositionHeader.length - 1];
      const url = window.URL.createObjectURL(new Blob([downloadedFile.blob]));
      const link = document.createElement("a");
      link.setAttribute("download", `${fileName}`);
      link.href = url;
      document.body.appendChild(link);
      link.click();
      link.remove();
      // 释放blob资源避免内存泄漏
      window.URL.revokeObjectURL(url);
    } finally {
      hideLoading();
      setIsDownloading(false);
    }
  }, [jarvisSdk, fieldType, showLoading, hideLoading, isDownloading]);

  return (
    <>
      {view?.length ? (
        <div className="App">
          <div className="Card">
            <div className="container">
              <Accordion>
                {view.map((item) => (
                  <AccordionItem
                    key={item.formId}
                    title={
                      <span className="text-decoration">
                        {item.formName}
                      </span>
                    }
                    onExpand={autoScroll}
                    onCollapse={autoScroll}
                  >
                    <ShowImgList
                      key={item.formId}
                      images={item.images}
                      onDownloadFile={onDownloadFile}
                      sdk={sdk}
                    />
                  </AccordionItem>
                ))}
              </Accordion>
            </div>
          </div>
        </div>
      ) : (
        <div className="App">
          <div className="Card">
            <div className="container">
              <div className="dropZoneContainer">
                <div className="Dropzone">
                  <Subheading className="d-color">暂无数据</Subheading>
                </div>
              </div>
            </div>
          </div>
        </div>
      )}
    </>
  );
};

const propTypes = {
  sdk: PropTypes.instanceOf(Object),
};

ShowFormsList.defaultProps = {
  sdk: undefined,
};

ShowFormsList.propTypes = propTypes;

export default ShowFormsList;

ShowImgList.jsx

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableRow,
  TabPanel,
  Icon,
} from "@contentful/forma-36-react-components";
import React, { useEffect, memo } from "react";
import PropTypes from "prop-types";

// 用memo包裹组件避免不必要重渲染
const ShowImgList = memo((props) => {
  const { images, onDownloadFile, sdk } = props;
  // 新增依赖数组,仅组件挂载或sdk变化时执行
  useEffect(() => {
    sdk.window.startAutoResizer();
  }, [sdk]);
  const defaultTab = "first";
  return (
    <div className="fileVersionContainer">
      <TabPanel id={defaultTab} className="versionContainer">
        <div>
          <Table>
            <TableHead>
              <TableRow>
                <TableCell>图片</TableCell>
                <TableCell>文件名</TableCell>
                <TableCell>类型</TableCell>
                <TableCell>下载</TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {images?.length ? images.map((aImg) => (
                <TableRow key={aImg.id}>
                  <TableCell key={`${aImg.id}cell`}>
                    <Icon key={`${aImg.id}asset`} icon="Asset" />
                  </TableCell>
                  <TableCell key={`-name-${aImg.id}`}>
                    {aImg.imageName}
                  </TableCell>
                  <TableCell key={`-type-${aImg.id}`}>
                    {aImg.extension}
                  </TableCell>
                  <TableCell key={`-download-${aImg.id}`}>
                    <Icon
                      icon="Download"
                      className="cursor-pointer"
                      onClick={() => onDownloadFile(aImg.id)}
                    />
                  </TableCell>
                </TableRow>
              )) : null}
            </TableBody>
          </Table>
        </div>
      </TabPanel>
    </div>
  );
});

const propTypes = {
  sdk: PropTypes.instanceOf(Object),
  images: PropTypes.instanceOf(Array),
  onDownloadFile: PropTypes.func,
};

ShowImgList.defaultProps = {
  sdk: undefined,
  images: undefined,
  onDownloadFile: undefined,
};

ShowImgList.propTypes = propTypes;

export default ShowImgList;

修复说明

  • 给所有useEffect补充依赖数组,确保副作用逻辑仅在依赖变化时执行,避免无限重渲染
  • 使用useCallback缓存函数实例、memo包裹子组件,减少不必要的组件重渲染
  • 新增下载状态锁,同一时间仅允许一个下载请求执行,避免用户快速点击导致重复下载
  • 补充blob资源释放逻辑,避免内存泄漏
  • 优化空值判断写法,简化代码逻辑

内容的提问来源于stack exchange,提问作者Rishikesh Bhardwaj

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.04 22:27:04