title: ReactJS: Download CSV File on Button Click
body: <p>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.</p>
  
  <p>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.</p>
  
  <p>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.</p>
  
  <p><a href="https://i.stack.imgur.com/ACIFV.png" rel="noreferrer"><img src="https://i.stack.imgur.com/ACIFV.png" alt="Example of survey results"></a></p>
  
  <p>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.</p>
  
  <p><a href="https://i.stack.imgur.com/Qa2tV.png" rel="noreferrer"><img src="https://i.stack.imgur.com/Qa2tV.png" alt="Results download in the list"></a></p>
  
  <p>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.</p>
  
  <p>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.</p>
  
  <pre><code>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 =&gt; ({
      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) &amp;&amp;
              (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 (&lt;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"
          &gt;
              &lt;Button
                  className={this.props.classes.button}
                  color="primary"
                  onClick={() =&gt; this.getSurveyReport(this.props.surveyId)}
                  size={'small'}
                  variant="raised"
              &gt;
                  Download Results
              &lt;/Button&gt;
          &lt;/CSVLink&gt;);
      }
  }
  
  export default withStyles(styles)(SurveyResults);
  </code></pre>
  
  <p>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.</p>
  
  <p>I have been trying to make it work by using the CSVDownload component. (that and CSVLink are both in this package: <a href="https://www.npmjs.com/package/react-csv" rel="noreferrer">https://www.npmjs.com/package/react-csv</a> )</p>
  
  <p>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.</p>
  
  <pre><code>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 =&gt; ({
      button: {
          margin: theme.spacing.unit,
          color: '#FFF !important',
      },
  });
  
  const getReportData = (surveyId) =&gt; {
      const reportData = getMockReport(surveyId);
      return reportData;
  };
  
  const DownloadReport = props =&gt; (
      &lt;div&gt;
          &lt;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"
          &gt;
              Download Results
          &lt;/Button&gt;
          &lt;If condition={props.allowDownload}&gt;&lt;DownloadCSV reportData={getReportData(this.props.surveyId)} target="_blank" /&gt;&lt;/If&gt;
      &lt;/div&gt;);
  
  export default withStyles(styles)(DownloadReport);
  </code></pre>
  
  <p>Render CSVDownload here:</p>
  
  <pre><code>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 =&gt; (
      &lt;CSVDownload
          headers={props.reportData.headers}
          data={props.reportData.data}
          target="_blank"
          // no way to specify the name of the file
      /&gt;);
  
  export default DownloadCSV;
  </code></pre>
  
  <p>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.</p>
  
  <p>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?</p>
  
  <p>I appreciate any help. Thank you!</p>