javascript ReactJS:单击按钮下载 CSV 文件
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/53504924/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
ReactJS: Download CSV File on Button Click
提问by JasonG
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.
本质上,我想通过单击按钮下载 CSV 文件。我正在为按钮使用 Material-UI 组件,并希望尽可能将功能与 React 紧密联系在一起,仅在绝对必要时才使用 vanilla JS。
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.
为了提供有关特定问题的更多背景信息,我有一份调查清单。每个调查都有一定数量的问题,每个问题有 2-5 个答案。一旦不同的用户回答了调查,网站管理员应该能够点击下载报告的按钮。此报告是一个 CSV 文件,其中包含与每个问题相关的标题以及显示选择每个答案的人数的相应数字。
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.
显示下载 CSV 按钮的页面是一个列表。该列表显示有关每个调查的标题和信息。因此,该行中的每个调查都有自己的下载按钮。
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.
每个调查都有一个与之关联的唯一 ID。此 ID 用于获取后端服务并提取相关数据(仅针对该调查),然后将其转换为适当的 CSV 格式。由于列表中可能包含数百个调查,因此只有在每次单击相应调查的按钮时才能获取数据。
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.
我曾尝试使用多个库,例如 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);
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.
我一直面临的问题是,直到第二次单击按钮,状态才会正确更新。更糟糕的是,当 this.state.data 作为 prop 传递到 CSVLink 时,它总是一个空数组。下载的 CSV 中未显示任何数据。最终,这似乎不是最好的方法。无论如何,我不喜欢为每个按钮设置一个隐藏组件的想法。
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)
我一直在尝试使用 CSVDownload 组件使其工作。(那个和 CSVLink 都在这个包中: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.
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);
Render CSVDownload here:
渲染 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;
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.
这里的问题是无法指定 CSV 的文件名。它似乎也不能每次都可靠地下载文件。事实上,它似乎只有在第一次点击时才会这样做。它似乎也没有提取数据。
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?
我考虑过使用 json2csv 和 js-file-download 包的方法,但我希望避免使用 vanilla JS 并只坚持使用 React。担心这件事好吗?这两种方法之一似乎也应该有效。有没有人以前解决过这样的问题并对解决它的最佳方法有明确的建议?
I appreciate any help. Thank you!
我很感激任何帮助。谢谢!
回答by JasonG
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:
并使用每个 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
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.
我还建议阅读React 中的 refs 文档。Refs 非常适合解决各种问题,但只应在必要时使用。
Hopefully this helps anybody else struggling with a solution to this problem!
希望这可以帮助其他人努力解决这个问题!
回答by siddiq
Concerning this solution herea 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
并使用动态 ID 调用如下所示的组件
import MyCsvLink from './MyCsvLink';//imported at the top
<MyCsvLink id={user.id} filename={user.name} /> //Use the component where required
回答by steve-o
A much more simple solution is to use the library https://www.npmjs.com/package/export-to-csv.
一个更简单的解决方案是使用库https://www.npmjs.com/package/export-to-csv。
Have a standard onClickcallback function on your button that prepares the json data you want to export to csv.
onClick在您的按钮上有一个标准的回调函数,用于准备要导出到 csv 的 json 数据。
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!
很快!


