javascript - ReactJS:单击按钮下载 CSV 文件

标签 javascript html reactjs csv material-ui

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

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

为了提供有关特定问题的更多背景信息,我有一份调查 list 。每个调查都有一定数量的问题,每个问题有 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 包的方法,但我希望避免使用 vanilla JS 并坚持只使用 React。这是一件好事吗?这两种方法中的一种似乎也应该起作用。有没有人以前解决过这样的问题并对解决它的最佳方法有明确的建议?

我很感激任何帮助。谢谢!

最佳答案

关于如何做到这一点有一个很好的答案 herereact-csv 问题线程上。我们的代码库是用带有钩子(Hook)的“现代”风格编写的。以下是我们如何改编该示例:

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,但你会实现完全相同的想法)

关于javascript - ReactJS:单击按钮下载 CSV 文件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53504924/

相关文章:

javascript - React——点击按钮定时器开始计时

Javascript:在 DOM 中异步生成大表。

html - 如何在CSS中重新定位响应式导航

javascript - 在 React JS 中使用 fetch 处理响应状态

reactjs - 胜利图与轴和刻度值之间的间距有关

javascript - 在socket io中的事件回调函数中获取事件名称

全局范围内的 JavaScript 数字构造方法。他们为什么工作?

jquery 动画无法工作

html - Reactjs - 调整大小时订购div

javascript - React setState 重新渲染