繁体   English   中英

ReactJS:单击按钮下载 CSV 文件

[英]ReactJS: Download CSV File on Button Click

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

本质上,我想通过单击按钮下载 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 作为 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。 担心这件事好吗? 这两种方法之一似乎也应该有效。 有没有人以前解决过这样的问题,并对解决问题的最佳方法有明确的建议?

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

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

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

为了正确返回数据,需要一个指向链接的引用。

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

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

此处发布了类似的解决方案,尽管不完全相同。 值得一读。

我还建议阅读React 中的 refs 文档 Refs 非常适合解决各种问题,但只应在必须使用时使用。

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

一个更简单的解决方案是使用库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);

和快!

在此处输入图片说明

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

我创建了一个组件如下

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

同样的问题,我的解决方案如下:(如@aaron answer)

  1. 使用引用
  2. CSV链接
  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>
        )
    }

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

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

setTimeout(() => { 

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM