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

React MUI DataGrid单行列图标更新异常求助

问题:MUI DataGrid下载按钮加载状态全局生效,而非仅目标行

我正在开发一款SFTP文件上传工具,可与S3进行文件上传/下载,使用MUI DataGrid组件。点击下载/上传按钮能正常操作对应行的文件,但点击下载后,所有行的下载图标都变成加载图标,而不是只有点击的那一行。我已经用了useCallback和useMemo,但问题依然存在,求指出问题所在。


原ListFiles组件代码

import { FileDownload } from "@mui/icons-material";
import { CssBaseline, IconButton } from "@mui/material";
import { DataGrid } from "@mui/x-data-grid";
import { toast } from "react-toastify";

import React, { useCallback, useEffect, useState } from "react";

import type { FileDetailsType, ListFilesProps } from "../../Types/Types";
import { downloadFile } from "../../api/fileAPI";
import Spinner from "../../utils/Spinner/Spinner";
import {
    errorToastOptions,
    successToastOptions
} from "../../utils/Toast/ToastOptions";

const ListFiles = ({ files }: ListFilesProps) => {
    const [fileDownloading, setFileDownloading] = useState(false);
    const [rows, setRows] = useState([]);


    useEffect(() => {
        setRows(
            files?.map((file) => {
                return {
                    name: file.name,
                    lastAccessedTime: file.lastAccessedTime,
                    lastModifiedTime: file.lastModifiedTime
                };
            })
        );
    }, [files]);

    const handleFileDownload = React.useCallback(
        async (fileData: FileDetailsType) => {
            try {
                setFileDownloading(true);
                const response = await downloadFile(fileData);
                if (response) {
                    const blob = new Blob([
                        response.data as unknown as BlobPart
                    ]);
                    const downloadURL = URL.createObjectURL(blob);
                    const tempLink = document.createElement("a");
                    tempLink.href = downloadURL;
                    tempLink.download = fileData?.name;
                    tempLink.click();
                    toast.success(
                        "File Download Successfully",
                        successToastOptions
                    );
                    setFileDownloading(false);
                } else {
                    toast.error("File Download Failed", errorToastOptions);
                    setFileDownloading(false);
                }
            } catch (error) {
                toast.error("File Download Failed", errorToastOptions);
                setFileDownloading(false);
            }
        },
        []
    );

    const renderRowBtn = useCallback(
        (params) => {
            return (
                <IconButton onClick={() => handleFileDownload(params.row)}>
                    {fileDownloading ? <Spinner /> : <FileDownload />}
                </IconButton>
            );
        },
        []
    );

    const columns = React.useMemo(
        () => [
            { field: "name", headerName: "Name", flex: 2 },
            { field: "lastAccessedTime", headerName: "Last Accessed", flex: 1 },
            { field: "lastModifiedTime", headerName: "Last Modified", flex: 1 },
            {
                field: "type",
                headerName: "",
                flex: 1,
                renderCell: renderRowBtn
            }
        ],
        []
    );

    return (
        <React.Fragment>
            <CssBaseline />
            <div style={{ width: "100%" }}>
                <DataGrid
                    columns={columns}
                    rows={rows}
                    getRowId={(row) => row.name}
                />
            </div>
        </React.Fragment>
    );
};

export default ListFiles;

原fileAPI代码

import axios from "axios";

import type { FileDetailsType } from "../Types/Types";

export const checkIsConnected = async () => {
    return await axios.get(`${process.env.API_URL}/api/isconnected`);
};

export const listFiles = async () => {
    return await axios.get(
        `${process.env.API_URL}/api/listchildnodes?folderPath=/`
    );
};

export const downloadFile = async (fileData: FileDetailsType) => {
    return await axios.get(
        `${process.env.API_URL}/api/download?filePath=${fileData?.name}`,
        { responseType: "arraybuffer" }
    );
};

export const uploadFile = async (formData, xsrfToken) => {
    return await axios.post(`${process.env.API_URL}/api/upload`, formData, {
        headers: { "X-XSRF-TOKEN": xsrfToken }
    });
};

问题根源

你用了一个全局布尔值fileDownloading控制所有行的加载状态,一旦设为true,所有行的按钮都会切换成加载图标。同时,renderRowBtn和columns的依赖数组不完整,导致组件无法感知状态变化,状态更新后按钮也不会重新渲染。

修改方案

  1. 将fileDownloading改为存储当前正在下载的文件名(唯一标识),精准定位目标行。
  2. 更新handleFileDownload,开始下载时记录文件名,结束时清空。
  3. 补全renderRowBtn和columns的依赖数组,确保状态变化时触发重渲染。

修改后的ListFiles组件代码

import { FileDownload } from "@mui/icons-material";
import { CssBaseline, IconButton } from "@mui/material";
import { DataGrid } from "@mui/x-data-grid";
import { toast } from "react-toastify";

import React, { useCallback, useEffect, useState } from "react";

import type { FileDetailsType, ListFilesProps } from "../../Types/Types";
import { downloadFile } from "../../api/fileAPI";
import Spinner from "../../utils/Spinner/Spinner";
import {
    errorToastOptions,
    successToastOptions
} from "../../utils/Toast/ToastOptions";

const ListFiles = ({ files }: ListFilesProps) => {
    // 改为存储正在下载的文件名,初始值为null
    const [downloadingFileName, setDownloadingFileName] = useState<string | null>(null);
    const [rows, setRows] = useState([]);


    useEffect(() => {
        setRows(
            files?.map((file) => {
                return {
                    name: file.name,
                    lastAccessedTime: file.lastAccessedTime,
                    lastModifiedTime: file.lastModifiedTime
                };
            })
        );
    }, [files]);

    const handleFileDownload = useCallback(
        async (fileData: FileDetailsType) => {
            try {
                // 记录当前正在下载的文件名
                setDownloadingFileName(fileData.name);
                const response = await downloadFile(fileData);
                if (response) {
                    const blob = new Blob([
                        response.data as unknown as BlobPart
                    ]);
                    const downloadURL = URL.createObjectURL(blob);
                    const tempLink = document.createElement("a");
                    tempLink.href = downloadURL;
                    tempLink.download = fileData?.name;
                    tempLink.click();
                    toast.success(
                        "文件下载成功",
                        successToastOptions
                    );
                } else {
                    toast.error("文件下载失败", errorToastOptions);
                }
            } catch (error) {
                toast.error("文件下载失败", errorToastOptions);
            } finally {
                // 无论成功失败,都清空正在下载的文件名
                setDownloadingFileName(null);
            }
        },
        []
    );

    const renderRowBtn = useCallback(
        (params) => {
            // 判断当前行的文件名是否等于正在下载的文件名
            const isDownloading = params.row.name === downloadingFileName;
            return (
                <IconButton onClick={() => handleFileDownload(params.row)}>
                    {isDownloading ? <Spinner /> : <FileDownload />}
                </IconButton>
            );
        },
        // 加入依赖,确保状态变化时重新渲染按钮
        [downloadingFileName, handleFileDownload]
    );

    const columns = useMemo(
        () => [
            { field: "name", headerName: "文件名", flex: 2 },
            { field: "lastAccessedTime", headerName: "最后访问时间", flex: 1 },
            { field: "lastModifiedTime", headerName: "最后修改时间", flex: 1 },
            {
                field: "actions",
                headerName: "操作",
                flex: 1,
                renderCell: renderRowBtn
            }
        ],
        // columns依赖renderRowBtn,加入到依赖数组
        [renderRowBtn]
    );

    return (
        <React.Fragment>
            <CssBaseline />
            <div style={{ width: "100%" }}>
                <DataGrid
                    columns={columns}
                    rows={rows}
                    getRowId={(row) => row.name}
                />
            </div>
        </React.Fragment>
    );
};

export default ListFiles;

关键修改说明

  • 状态精准化:从全局布尔值改为存储具体文件名,实现单一行的加载状态控制。
  • 可靠的状态重置:用finally块确保无论请求成功或失败,都能重置下载状态,避免永久加载。
  • 完整的依赖数组:补全renderRowBtn和columns的依赖,保证React能正确触发重渲染,更新按钮状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 17:08:11