簡體   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