web-dev-qa-db-ja.com

ReactJS:ボタンをクリックしてCSVファイルをダウンロード

このトピックにはいくつかの投稿がありますが、どれも私の問題を解決するようには見えません。希望する結果を得るために、いくつかの異なるライブラリ、さらにはライブラリの組み合わせを使用してみました。私はこれまでのところ運がありませんが、解決策に非常に近いと感じています。

基本的に、ボタンをクリックするだけでCSVファイルをダウンロードしたいと思います。ボタンにMaterial-UIコンポーネントを使用していますが、機能をReactにできるだけ密接に関連付け、絶対に必要な場合にのみVanilla JSを使用します。

特定の問題についてもう少しコンテキストを提供するために、私は調査のリストを持っています。各調査には設定された数の質問があり、各質問には2〜5個の回答があります。さまざまなユーザーがアンケートに回答すると、Webサイトの管理者はレポートをダウンロードするボタンをクリックできるようになります。このレポートは、各質問に関連するヘッダーと、各回答を選択した人数を示す対応する番号を含むCSVファイルです。

Example of survey results

ダウンロードCSVボタンが表示されるページはリストです。リストには、各調査のタイトルと情報が表示されます。そのため、行の各調査には独自のダウンロードボタンがあります。

Results download in the list

各調査には固有の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);

私が直面している問題は、ボタンを2回クリックするまで状態が正しく更新されないことです。さらに悪いことに、this.state.dataがプロップとして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);

ここでCSVDownloadをレンダリング:

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パッケージを使用するアプローチを取ることを検討しましたが、私はバニラJSの使用を避け、Reactのみに固執することを望んでいました。 ?また、これらの2つのアプローチのいずれかが機能するように思われます。

どんな助けにも感謝します。ありがとうございました!

6
JasonG

この質問が過去数か月にわたって多くのヒットを得ていることに気づきました。他の人がまだ答えを探している場合のために、ここで私のために働いた解決策があります。

データが正しく返されるためには、リンクを指す参照が必要でした。

親コンポーネントの状態を設定するときに定義します。

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での参照のドキュメント もお勧めします。 Refはさまざまな問題を解決するのに最適ですが、必要な場合にのみ使用してください。

うまくいけば、これがこの問題の解決に苦労している他の人を助けるでしょう!

2
JasonG

より簡単な解決策は、ライブラリ 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);

そしてプレスト!

enter image description here

0
steve-o

この解決策について here 以下の少し変更されたコードが私のために働きました。クリックでデータを取得し、最初にファイルをダウンロードします。

以下のようにコンポーネントを作成しました

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
0
siddiq