ReactJS:单击按钮下载CSV文件

时间:2018-11-27 17:19:20

标签: javascript html reactjs csv material-ui

围绕该主题有几篇文章,但似乎都无法解决我的问题。为了获得所需的结果,我尝试使用几种不同的库,甚至库的组合。到目前为止,我还没有运气,但感觉非常接近解决方案。

基本上,我想单击一个按钮即可下载CSV文件。我正在为按钮使用Material-UI组件,并希望使功能尽可能与React紧密联系,仅在绝对必要时才使用香草JS。

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

Example of survey results

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

Results download in the list

每个调查都具有唯一的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作为prop传递给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软件包的方法,但是我希望避免使用香草JS,而只坚持使用React。这是一件好事吗?这两种方法之一似乎也应该起作用。以前有没有人解决过这样的问题,并提出了关于解决该问题的最佳方法的明确建议?

感谢您的帮助。谢谢!

5 个答案:

答案 0 :(得分:1)

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

按钮上具有标准的import matplotlib.pyplot as plt def function(ax=None): ax=ax or plt.gca() sc = ax.scatter([1,2,3], [1,3,2]) ax.figure.colorbar(sc) return sc fig, ax = plt.subplots() sc = function(ax=ax) sc.colorbar.set_label("My Label") plt.show() 回调函数,该函数准备要导出到csv的json数据。

设置您的选项:

onClick

然后打电话

      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!
      };

然后保存!

enter image description here

答案 1 :(得分:1)

react-csv问题线程中,如何执行here有一个很好的答案。我们的代码库是带有钩子的“现代”风格的代码。这是我们改编该示例的方式:

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而不是实质性的ui,但是您将实现完全相同的想法)

答案 2 :(得分:0)

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

需要指向该链接的引用才能正确返回数据。

在设置父组件的状态时定义它:

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。引用很适合解决各种问题,但仅在必须使用时才应使用。

希望这可以帮助其他苦苦解决该问题的人!

答案 3 :(得分:0)

关于此解决方案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

答案 4 :(得分:0)

相同的问题和我的解决方法如下:(例如@aaron答案)

  1. useRef
  2. CSVLink
  3. 当用户单击按钮时获取数据
    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>
        )
    }