Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ReactJS: Download CSV File on Button Click

There are several posts around this topic, but none of them quite seem to solve my problem. I have tried using several different libraries, even combinations of libraries, in order to get the desired results. I have had no luck so far but feel very close to the solution.

Essentially, I want to download a CSV file on the click of a button. I am using Material-UI components for the button and would like to keep the functionality as closely tied to React as possible, only using vanilla JS if absolutely necessary.

To provide a little more context about the specific problem, I have a list of surveys. Each survey has a set number of questions and each question has 2-5 answers. Once different users have answered the surveys, the admin of the website should be able to click a button that downloads a report. This report is a CSV file with headers that pertain to each question and corresponding numbers which show how many people selected each answer.

Example of survey results

The page the download CSV button(s) are displayed on is a list. The list shows the titles and information about each survey. As such, each survey in the row has its own download button.

Results download in the list

Each survey has a unique id associated with it. This id is used to make a fetch to the backend service and pull in the relevant data (for that survey only), which is then converted to the appropriate CSV format. Since the list may have hundreds of surveys in it, the data should only be fetched with each individual click on the corresponding survey's button.

I have attempted using several libraries, such as CSVLink and json2csv. My first attempt was using CSVLink. Essentially, the CSVLink was hidden and embedded inside of the button. On click of the button, it triggered a fetch, which pulled in the necessary data. The state of the component was then updated and the CSV file downloaded.

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);

The problem I kept facing is that the state would not update properly until the second click of the button. Even worse, when this.state.data was being passed into CSVLink as a prop, it was always an empty array. No data was showing up in the downloaded CSV. Eventually, it seemed like this may not be the best approach. I did not like the idea of having a hidden component for each button anyway.

I have been trying to make it work by using the CSVDownload component. (that and CSVLink are both in this package: https://www.npmjs.com/package/react-csv )

The DownloadReport component renders the Material-UI button and handles the event. When the button is clicked, it propagates the event several levels up to a stateful component and changes the state of allowDownload. This in turn triggers the rendering of a CSVDownload component, which makes a fetch to get the specified survey data and results in the CSV being downloaded.

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);

Render CSVDownload here:

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;

A problem here is that the file name of the CSV cannot be specified. It also does not seem to reliably download the file each time. In fact, it only seems to do it on the first click. It does not seem to be pulling in the data either.

I have considered taking an approach using the json2csv and js-file-download packages, but I was hoping to avoid using vanilla JS and stick to React only. Is that an okay thing to be concerned about? It also seems like one of these two approaches should work. Has anyone tackled a problem like this before and have a clear suggestion on the best way to solve it?

I appreciate any help. Thank you!

like image 350
JasonG Avatar asked Nov 27 '18 17:11

JasonG


People also ask

How do I download a CSV file using react?

You can use the Show CSV export content text button, to preview the output. You can use the Download CSV export file button to download a csv file. The file will be exported using the default name: export. csv .

What are csv files?

A CSV (comma-separated values) file is a text file that has a specific format which allows data to be saved in a table structured format.


1 Answers

There's a great answer to how to do this here on the react-csv issues thread. Our code base is written in the "modern" style with hooks. Here's how we adapted that example:

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>
  )
}

(we use react bootstrap instead of material ui, but you'd implement exactly the same idea)

like image 112
aaron Avatar answered Oct 12 '22 01:10

aaron