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
相关产品推荐
相关产品推荐

