Reputation: 500
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.
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.
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!
Upvotes: 25
Views: 112600
Reputation: 13
In case, the button downloads an empty CSV file and on the second click it downloads the data from the previous fetch, put your this.csvLink.current.link.click() inside of the setTimeout statement like so:
this.setState({ data : reportData}, () => {
setTimeout(() => {
this.csvLink.current.link.click()
});
});
Upvotes: 0
Reputation: 31
Same problems and my solution are as follows: (like @aaron answer)
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>
)
}
Upvotes: 2
Reputation: 6489
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)
Upvotes: 26
Reputation: 1403
A much more simple solution is to use the library https://www.npmjs.com/package/export-to-csv.
Have a standard onClick
callback function on your button that prepares the json data you want to export to csv.
Set your options:
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!
};
then call
const csvExporter = new ExportToCsv(options);
csvExporter.generateCsv(data);
and presto!
Upvotes: 12
Reputation: 1711
Concerning this solution here a little modified code below worked for me. It will fetch the data on click and download the file at first time itself.
I created a component as below
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;
and call the component like below with the dynamic id
import MyCsvLink from './MyCsvLink';//imported at the top
<MyCsvLink id={user.id} filename={user.name} /> //Use the component where required
Upvotes: 3
Reputation: 500
I have noticed that this question has been getting many hits over the past several months. In case others are still looking for answers, here is the solution which worked for me.
A ref pointing back to the link was required in order for the data to be returned correctly.
Define it when setting the state of the parent component:
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()
});
}
And render it with each CSVLink component:
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
A similar solution was posted here, albeit not entirely the same. It is worth reading.
I would also recommend reading the documentation for refs in React. Refs are great for solving a variety of problems but should only be used when they have to be.
Hopefully this helps anybody else struggling with a solution to this problem!
Upvotes: 5