web-dev-qa-db-fra.com

ReactJS: Télécharger le fichier CSV en cliquant sur le bouton

Il y a plusieurs articles sur ce sujet, mais aucun d'eux ne semble tout à fait résoudre mon problème. J'ai essayé d'utiliser plusieurs bibliothèques différentes, même des combinaisons de bibliothèques, afin d'obtenir les résultats souhaités. Je n'ai pas eu de chance jusqu'à présent mais je me sens très proche de la solution.

Essentiellement, je veux télécharger un fichier CSV en cliquant sur un bouton. J'utilise des composants Material-UI pour le bouton et je voudrais garder la fonctionnalité aussi étroitement liée à React que possible, en utilisant uniquement Vanilla JS si cela est absolument nécessaire).

Pour fournir un peu plus de contexte sur le problème spécifique, j'ai une liste d'enquêtes. Chaque enquête a un nombre défini de questions et chaque question a 2-5 réponses. Une fois que différents utilisateurs ont répondu aux sondages, l'administrateur du site Web devrait pouvoir cliquer sur un bouton qui télécharge un rapport. Ce rapport est un fichier CSV avec des en-têtes qui se rapportent à chaque question et des numéros correspondants qui indiquent combien de personnes ont sélectionné chaque réponse.

Example of survey results

La page sur laquelle les boutons CSV de téléchargement sont affichés est une liste. La liste affiche les titres et les informations sur chaque enquête. En tant que tel, chaque enquête de la ligne possède son propre bouton de téléchargement.

Results download in the list

Chaque enquête est associée à un identifiant unique. Cet identifiant est utilisé pour effectuer une extraction vers le service principal et extraire les données pertinentes (pour cette enquête uniquement), qui sont ensuite converties au format CSV approprié. Étant donné que la liste peut contenir des centaines d'enquêtes, les données ne doivent être récupérées qu'à chaque clic individuel sur le bouton de l'enquête correspondante.

J'ai essayé d'utiliser plusieurs bibliothèques, telles que CSVLink et json2csv. Ma première tentative a été d'utiliser CSVLink. Essentiellement, le CSVLink était caché et intégré à l'intérieur du bouton. En cliquant sur le bouton, il a déclenché une extraction, qui a extrait les données nécessaires. L'état du composant a ensuite été mis à jour et le fichier CSV téléchargé.

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

Le problème auquel je faisais face était que l'état ne se mettait pas à jour correctement jusqu'au deuxième clic du bouton. Pire encore, lorsque this.state.data était transmis à CSVLink en tant qu'accessoire, il s'agissait toujours d'un tableau vide. Aucune donnée n'apparaissait dans le CSV téléchargé. Finalement, il semblait que ce n'était peut-être pas la meilleure approche. Je n'aimais pas l'idée d'avoir un composant caché pour chaque bouton de toute façon.

J'ai essayé de le faire fonctionner en utilisant le composant CSVDownload. (cela et CSVLink sont tous les deux dans ce package: https://www.npmjs.com/package/react-csv )

Le composant DownloadReport rend le bouton Material-UI et gère l'événement. Lorsque le bouton est cliqué, il propage l'événement sur plusieurs niveaux jusqu'à un composant avec état et modifie l'état de allowDownload. Cela déclenche à son tour le rendu d'un composant CSVDownload, qui effectue une extraction pour obtenir les données d'enquête et les résultats spécifiés dans le téléchargement du 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);

Rendre CSVDownload ici:

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;

Un problème ici est que le nom de fichier du CSV ne peut pas être spécifié. Il ne semble pas non plus pouvoir télécharger le fichier de manière fiable à chaque fois. En fait, il ne semble le faire qu'au premier clic. Il ne semble pas non plus extraire les données.

J'ai envisagé d'adopter une approche utilisant les packages json2csv et js-file-download, mais j'espérais éviter d'utiliser Vanilla JS et m'en tenir à React uniquement. Est-ce une chose acceptable à se soucier de "Il semble également que l'une de ces deux approches devrait fonctionner. Quelqu'un at-il déjà abordé un problème comme celui-ci et a-t-il une suggestion claire sur la meilleure façon de le résoudre?

J'apprécie toute aide. Je vous remercie!

6
JasonG

J'ai remarqué que cette question a reçu de nombreux succès au cours des derniers mois. Au cas où d'autres chercheraient toujours des réponses, voici la solution qui a fonctionné pour moi.

Une référence pointant vers le lien était requise pour que les données soient renvoyées correctement.

Définissez-le lors de la définition de l'état du composant parent:

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

Et rendez-le avec chaque composant 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

Une solution similaire a été publiée ici , mais pas entièrement la même. Cela vaut la peine d'être lu.

Je recommanderais également de lire la documentation pour les références dans React . Les références sont parfaites pour résoudre une variété de problèmes, mais ne doivent être utilisées que lorsqu'elles doivent l'être.

Espérons que cela aide quiconque à trouver une solution à ce problème!

2
JasonG

Une solution beaucoup plus simple consiste à utiliser la bibliothèque https://www.npmjs.com/package/export-to-csv .

Avoir une fonction de rappel standard onClick sur votre bouton qui prépare les données json que vous souhaitez exporter vers csv.

Définissez vos 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!
      };

puis appelez

const csvExporter = new ExportToCsv(options);
csvExporter.generateCsv(data);

et hop!

enter image description here

0
steve-o

Concernant cette solution ici un petit code modifié ci-dessous a fonctionné pour moi. Il récupérera les données en un clic et téléchargera le fichier lui-même pour la première fois.

J'ai créé un composant comme ci-dessous

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;

et appelez le composant comme ci-dessous avec l'id dynamique

import MyCsvLink from './MyCsvLink';//imported at the top
<MyCsvLink id={user.id} filename={user.name} /> //Use the component where required
0
siddiq