ReactJS:单击按钮下载 CSV 文件

Posted

技术标签:

【中文标题】ReactJS:单击按钮下载 CSV 文件【英文标题】:ReactJS: Download CSV File on Button Click 【发布时间】:2019-04-29 12:05:20 【问题描述】:

围绕这个主题有几篇文章,但似乎没有一篇能完全解决我的问题。我尝试使用几个不同的库,甚至是库的组合,以获得所需的结果。到目前为止我没有运气,但感觉非常接近解决方案。

基本上,我想通过单击按钮下载 CSV 文件。我正在为按钮使用 Material-UI 组件,并希望尽可能使功能与 React 紧密相关,仅在绝对必要时才使用 vanilla JS。

为了提供有关特定问题的更多背景信息,我提供了一份调查列表。每个调查都有一定数量的问题,每个问题有 2-5 个答案。一旦不同的用户回答了调查,网站管理员应该能够单击下载报告的按钮。此报告是一个 CSV 文件,其中包含与每个问题相关的标题和显示有多少人选择了每个答案的相应数字。

显示下载 CSV 按钮的页面是一个列表。该列表显示每个调查的标题和信息。因此,该行中的每个调查都有自己的下载按钮。

每个调查都有一个与之关联的唯一 ID。此 id 用于获取后端服务并提取相关数据(仅用于该调查),然后将其转换为适当的 CSV 格式。由于列表中可能包含数百个调查,因此只能在每次单击相应调查的按钮时获取数据。

我曾尝试使用多个库,例如 CSVLink 和 json2csv。我的第一次尝试是使用 CSVLink。本质上,CSVLink 被隐藏并嵌入在按钮内。单击按钮时,它触发了提取,提取了必要的数据。然后更新组件的状态并下载 CSV 文件。

import React from 'react';
import Button from '@material-ui/core/Button';
import  withStyles  from '@material-ui/core/styles';
import  CSVLink  from 'react-csv';
import  getMockReport  from '../../../mocks/mockReport';

const styles = theme => (
    button: 
        margin: theme.spacing.unit,
        color: '#FFF !important',
    ,
);

class SurveyResults extends React.Component 
    constructor(props) 
        super(props);

        this.state =  data: [] ;

        this.getSurveyReport = this.getSurveyReport.bind(this);
    

    // Tried to check for state update in order to force re-render
    shouldComponentUpdate(nextProps, nextState) 
        return !(
            (nextProps.surveyId === this.props.surveyId) &&
            (nextState.data === this.state.data)
        );
    

    getSurveyReport(surveyId) 
        // this is a mock, but getMockReport will essentially be making a fetch
        const reportData = getMockReport(surveyId);
        this.setState( data: reportData );
    

    render() 
        return (<CSVLink
            style= textDecoration: 'none' 
            data=this.state.data
            // I also tried adding the onClick event on the link itself
            filename='my-file.csv'
            target="_blank"
        >
            <Button
                className=this.props.classes.button
                color="primary"
                onClick=() => this.getSurveyReport(this.props.surveyId)
                size='small'
                variant="raised"
            >
                Download Results
            </Button>
        </CSVLink>);
    


export default withStyles(styles)(SurveyResults);

我一直面临的问题是状态不会正确更新,直到第二次单击按钮。更糟糕的是,当 this.state.data 作为道具传递给 CSVLink 时,它始终是一个空数组。下载的 CSV 中未显示任何数据。最终,这似乎不是最好的方法。无论如何,我不喜欢每个按钮都有一个隐藏组件的想法。

我一直在尝试使用 CSVDownload 组件使其工作。 (那个和 CSVLink 都在这个包里:https://www.npmjs.com/package/react-csv)

DownloadReport 组件呈现 Material-UI 按钮并处​​理事件。单击按钮时,它将事件传播几个级别,直到有状态组件并更改 allowDownload 的状态。这反过来会触发 CSVDownload 组件的呈现,该组件进行提取以获取指定的调查数据并导致 CSV 被下载。

import React from 'react';
import Button from '@material-ui/core/Button';
import  withStyles  from '@material-ui/core/styles';
import DownloadCSV from 'Components/ListView/SurveyTable/DownloadCSV';
import  getMockReport  from '../../../mocks/mockReport';

const styles = theme => (
    button: 
        margin: theme.spacing.unit,
        color: '#FFF !important',
    ,
);

const getReportData = (surveyId) => 
    const reportData = getMockReport(surveyId);
    return reportData;
;

const DownloadReport = props => (
    <div>
        <Button
            className=props.classes.button
            color="primary"
            // downloadReport is defined in a stateful component several levels up
            // on click of the button, the state of allowDownload is changed from false to true
            // the state update in the higher component results in a re-render and the prop is passed down
            // which makes the below If condition true and renders DownloadCSV
            onClick=props.downloadReport
            size='small'
            variant="raised"
        >
            Download Results
        </Button>
        <If condition=props.allowDownload><DownloadCSV reportData=getReportData(this.props.surveyId) target="_blank" /></If>
    </div>);

export default withStyles(styles)(DownloadReport);

渲染 CSV 在此处下载:

import React from 'react';
import  CSVDownload  from 'react-csv';

// I also attempted to make this a stateful component
// then performed a fetch to get the survey data based on this.props.surveyId
const DownloadCSV = props => (
    <CSVDownload
        headers=props.reportData.headers
        data=props.reportData.data
        target="_blank"
        // no way to specify the name of the file
    />);

export default DownloadCSV;

这里的一个问题是无法指定 CSV 的文件名。它似乎也不能每次都可靠地下载文件。事实上,它似乎只在第一次点击时才会这样做。它似乎也没有提取数据。

我考虑过使用 json2csv 和 js-file-download 包的方法,但我希望避免使用 vanilla JS 并只使用 React。这是一件值得担心的事情吗?这两种方法中的一种似乎也应该起作用。有没有人曾经解决过这样的问题,并且对解决它的最佳方法有明确的建议?

感谢您的帮助。谢谢!

【问题讨论】:

您是否有理由不能只使用指向 csv 文件的链接?不需要额外的模块,如果用户在浏览器中单击指向 CSV 文件的链接,它将自动下载(除非他们有某种浏览器扩展来打开 csv 文件)。 【参考方案1】:

如果该按钮下载了一个空的 CSV 文件,并且在第二次单击时它会从上一次获取中下载数据,请将您的 this.csvLink.current.link.click() 放入setTimeout 语句如下:

this.setState( data : reportData, () =>  

setTimeout(() =>  

this.csvLink.current.link.click() 
); 
);

【讨论】:

【参考方案2】:

同样的问题和我的解决方法如下:(like @aaron answer)

    使用参考 CSV 链接 当用户点击按钮时获取数据
    import React,  useContext, useEffect, useState, useRef  from "react";
    import  CSVLink  from "react-csv";

    const [dataForDownload, setDataForDownload] = useState([]);
    const [bDownloadReady, setDownloadReady] = useState(false);

    useEffect(() => 
        if (csvLink && csvLink.current && bDownloadReady) 
            csvLink.current.link.click();
            setDownloadReady(false);
        
    , [bDownloadReady]);
    
    const handleAction = (actionType) => 
        if (actionType === 'DOWNLOAD') 
            //get data here
            setDataForDownload(newDataForDownload);
            setDownloadReady(true);
        
    
    
    const render = () => 
        return (
            <div>
                <button type="button" className="btn btn-outline-sysmode btn-sm" onClick=(e) => handleAction('DOWNLOAD')>Download</button>
                <CSVLink 
                    data=dataForDownload 
                    filename="data.csv"
                    className="hidden"
                    ref=csvLink
                    target="_blank" />
            </div>
        )
    

【讨论】:

【参考方案3】:

here 在react-csv 问题线程上有一个很好的答案。我们的代码库是用带有钩子的“现代”风格编写的。以下是我们如何调整该示例:

import React,  useState, useRef  from 'react'
import  Button  from 'react-bootstrap'
import  CSVLink  from 'react-csv'
import api from 'services/api'

const MyComponent = () => 
  const [transactionData, setTransactionData] = useState([])
  const csvLink = useRef() // setup the ref that we'll use for the hidden CsvLink click once we've updated the data

  const getTransactionData = async () => 
    // 'api' just wraps axios with some setting specific to our app. the important thing here is that we use .then to capture the table response data, update the state, and then once we exit that operation we're going to click on the csv download link using the ref
    await api.post('/api/get_transactions_table',  game_id: gameId )
      .then((r) => setTransactionData(r.data))
      .catch((e) => console.log(e))
    csvLink.current.link.click()
  

  // more code here

  return (
  // a bunch of other code here...
    <div>
      <Button onClick=getTransactionData>Download transactions to csv</Button>
      <CSVLink
         data=transactionData
         filename='transactions.csv'
         className='hidden'
         ref=csvLink
         target='_blank'
      />
    </div>
  )

(我们使用 react bootstrap 而不是 material ui,但你会实现完全相同的想法)

【讨论】:

【参考方案4】:

一个更简单的解决方案是使用库https://www.npmjs.com/package/export-to-csv。

在您的按钮上有一个标准的onClick 回调函数,用于准备您要导出到 csv 的 json 数据。

设置您的选项:

      const options =  
        fieldSeparator: ',',
        quoteStrings: '"',
        decimalSeparator: '.',
        showLabels: true, 
        showTitle: true,
        title: 'Stations',
        useTextFile: false,
        useBom: true,
        useKeysAsHeaders: true,
        // headers: ['Column 1', 'Column 2', etc...] <-- Won't work with useKeysAsHeaders present!
      ;

然后调用

const csvExporter = new ExportToCsv(options);
csvExporter.generateCsv(data);

马上!

【讨论】:

这应该是向上的【参考方案5】:

关于这个解决方案here,下面的一些修改代码对我有用。它会在点击时获取数据并在第一次自己下载文件。

我创建了一个组件如下

class MyCsvLink extends React.Component 
    constructor(props) 
        super(props);
        this.state =  data: [], name:this.props.filename?this.props.filename:'data' ;
        this.csvLink = React.createRef();
    



  fetchData = () => 
    fetch('/mydata/'+this.props.id).then(data => 
        console.log(data);
      this.setState( data:data , () => 
        // click the CSVLink component to trigger the CSV download
        this.csvLink.current.link.click()
      )
    )
  

  render() 
    return (
      <div>
        <button onClick=this.fetchData>Export</button>

        <CSVLink
          data=this.state.data
          filename=this.state.name+'.csv'
          className="hidden"
          ref=this.csvLink
          target="_blank" 
       />
    </div>
    )
  

export default MyCsvLink;

并使用动态 id 调用如下组件

import MyCsvLink from './MyCsvLink';//imported at the top
<MyCsvLink id=user.id filename=user.name /> //Use the component where required

【讨论】:

如果我们通过按钮下载 CSV 点击代码:“this.csvLink.current.link.click()”,下载失败,证书错误。知道如何解决这个问题。【参考方案6】:

我注意到这个问题在过去几个月里受到了很多人的欢迎。如果其他人仍在寻找答案,这是对我有用的解决方案。

需要一个指向链接的 ref 才能正确返回数据。

设置父组件状态时定义:

getSurveyReport(surveyId) 
    // this is a mock, but getMockReport will essentially be making a fetch
    const reportData = getMockReport(surveyId);
    this.setState( data: reportData , () => 
         this.surveyLink.link.click()
    );

并使用每个 CSVLink 组件进行渲染:

render() 
    return (<CSVLink
        style= textDecoration: 'none' 
        data=this.state.data
        ref=(r) => this.surveyLink = r
        filename='my-file.csv'
        target="_blank"
    >
    //... the rest of the code here

类似的解决方案was posted here,尽管不完全相同。值得一读。

我还建议阅读documentation for refs in React。 Refs 非常适合解决各种问题,但只能在必要时使用。

希望这可以帮助其他正在努力解决这个问题的人!

【讨论】:

感谢您的解决方案。你的问题和答案很简短。我和你有同样的问题。但是this.surveyLink.link.click() 是什么?如果可能的话,您能否按照您的问题发布完整的答案。 给那些混淆this.surveyLink. 的人的小提示。这应该在构造方法this.surveyLink = React.createRef();中声明如下 正在下载两个文件,第一次单击时第一个文件为空。但是从第二次单击开始,两个文件都有数据。会有什么问题?我检查了网络选项卡。两个请求被发送到服务器 URL。第一个是 OPTION 请求,第二个是 POST。有问题吗? 我尝试使用相同的方法在我的网站上下载 .csv,但下载的文件在某些​​列中包含 html 标签(例如

)。知道为什么会发生这种情况,我该如何解决这个问题?
我想将生成的 CSV 文件下载到给定文件夹中。任何指针都会有所帮助。

以上是关于ReactJS:单击按钮下载 CSV 文件的主要内容,如果未能解决你的问题,请参考以下文章

reactjs - 无法读取未定义的属性推送

reactjs如何下载文件

下载 csv 文件作为对 AJAX 请求的响应

将 csv 下载到服务器文件路径

测试按钮单击 ReactJS 上是不是呈现另一个组件

为什么我的csv下载按钮仅适用于Chrome