Descargar un objeto ReactJS como un archivo

9 minutos de lectura

avatar de usuario de fryeguy
alevines

Estoy creando una aplicación con un front-end ReactJS que se conecta a un servidor Express API. Las llamadas a la API se realizan mediante Ajax.

En una de mis vistas, se carga una tabla con enlaces “Exportar” en cada fila. Los enlaces de exportación conducen a una ruta React que llama a un punto final de API que proporciona un archivo CSV para descargar.

Si llego al punto final de la API directamente con una solicitud válida (fuera de la aplicación React), se inicia una descarga de archivo en mi navegador. ¡Perfecto! Sin embargo, al seguir el enlace Exportar desde la página de React, se intenta cargar la vista donde se produce la llamada a la API. La tabla desaparece de la vista y se reemplaza por el contenido del archivo (a propósito para demostrar que tengo los datos), pero no se descarga ningún archivo.

¿Puedo forzar una descarga del contenido del objeto de respuesta como un archivo? ¿Podría esto tener lugar en la devolución de llamada exitosa de ajax? Hice un intento con javascript pero estoy luchando con el DOM virtual de React… Supongo que esto debe ser bastante sencillo, pero estoy perplejo.

EDITAR: ¡Los comentarios de @Blex me ayudaron a resolver este problema! La solución se agrega al fragmento de código…

Aquí está el JSX que recibe los datos:

module.exports = React.createClass({

    mixins: [Router.State],
    getInitialState: function() {
        return {
            auth: getAuthState(),
            export: [],
            passedParams: this.getParams()
        };
    },

    componentDidMount: function(){
        $.ajax({
            type: 'GET',
            url: ''+ API_URL +'/path/to/endpoint'+ this.state.passedParams.id +'/export',
            dataType: 'text',
            headers: {
                'Authorization': 'Basic ' + this.state.auth.base + ''
            },
            success: function (res) {
                // can I force a download of res here?
                console.log('Export Result Success -- ', res);
                if(this.isMounted()){
                    console.log('Export Download Data -- ', res);
                    this.setState({export: res[1]});
                    // adding the next three lines solved my problem
                    var data = new Blob([res], {type: 'text/csv'});
                    var csvURL = window.URL.createObjectURL(data);
                    //window.open(csvURL);
                    // then commenting out the window.open & replacing
                    // with this allowed a file name to be passed out
                    tempLink = document.createElement('a');
                    tempLink.href = csvURL;
                    tempLink.setAttribute('download', 'filename.csv');
                    tempLink.click();
                }
            }.bind(this),
            error: function (data) {
                console.log('Export Download Result Error -- ', data);
            }
        });
    },

    render: function(){
        console.log('exam assignment obj -- ', this.state.passedParams.name);
        var theFileContents = this.state.export;
            return(
            <div className="row test-table">
                <table className="table" >
                    <tr className="test-table-headers">
                    {theFileContents} // this loads the contents
                    // can I auto download theFileContents?
                    </tr>
                </table>
            </div>
            )
    }
});

  • Puedes hacerlo de esta manera: jsfiddle.net/8f2ah406 El único problema aquí es que el nombre del archivo y la extensión deben configurarse manualmente cuando se le solicite. De lo contrario, solo window.open(/* URL of the Ajax request */);

    – blex

    3 de julio de 2015 a las 22:12


  • ¡Eso funcionó perfectamente! Ahora solo necesito descubrir cómo configurar el nombre del archivo automáticamente.

    – chico frito

    3 de julio de 2015 a las 22:22

  • Relacionado: stackoverflow.com/questions/14964035/…

    – Peeyush Kushwaha

    3 de julio de 2015 a las 22:24


  • Ahí tienes: jsfiddle.net/unmf5dp0

    – blex

    3 de julio de 2015 a las 22:28

  • Gracias @PeeyushKushwaha por el enlace. En javascript normal, creo que esto funciona bien. Sin embargo, estoy trabajando en un entorno React JS que abstrae el DOM de javascript del usuario. El concepto de un DOM virtual todavía me resulta bastante extraño y complica muchas tareas.

    – chico frito

    4 de julio de 2015 a las 3:59

avatar de usuario de fryeguy
alevines

Agregar el siguiente código basado en los comentarios de @blex hizo que la descarga del archivo funcionara. Para verlo en contexto, eche un vistazo a la devolución de llamada exitosa en la pregunta.

var data = new Blob([res], {type: 'text/csv'});
var csvURL = window.URL.createObjectURL(data);
tempLink = document.createElement('a');
tempLink.href = csvURL;
tempLink.setAttribute('download', 'filename.csv');
tempLink.click();

  • Si agrega tempLink al cuerpo, también funcionará en Firefox. No estoy seguro acerca de Safari.

    – jstice4all

    3 oct 2018 a las 15:38

  • No usaría este enfoque al menos para Reactjs porque volverá a generar todo el árbol dom.

    – Mahesh

    12 de septiembre de 2019 a las 5:00

  • agregar var por tempLink variable

    – sharun kk

    19 de enero de 2021 a las 12:16

  • ¿Este método va a manejar un archivo de cualquier tipo de tamaño real? Quiero decir, ¿crear un objeto como una URL? ¿Eso realmente va a manejar un archivo de 4GB?

    – Diggy John

    12 mayo 2021 a las 19:14

  • La publicación original fue hace cerca de seis años, pero si no recuerdo mal, el punto de lo que estaba tratando de lograr era proporcionar una opción para descargar los datos que se muestran en la página en formato CSV. Supongo que si pudiera ver los datos en la página, no veo ninguna razón por la que un archivo grande fallaría. Sin embargo, dudo que alguno de los datos con los que estaba trabajando aumentara más allá de varios MB, y mucho menos de un GB. Tal vez intente un caso de prueba.

    – chico frito

    19 de mayo de 2021 a las 2:21


Avatar de usuario de Neo
Neo

usé un paquete jsonexport en mi aplicación React y ahora puedo descargar el archivo csv con un clic en el enlace. Aquí esta lo que hice:

.
.
import React, {useState,useEffect} from 'react';// I am using React Hooks
import * as jsonexport from "jsonexport/dist";
.
.
.
const [filedownloadlink, setFiledownloadlink] = useState("");//To store the file download link

.
.
.

Cree una función que proporcione datos para CSV. También puede estar en una devolución de llamada de una solicitud de red. Cuando se llama a este método, establecerá el valor en filedownloadlink estado.

function handleSomeEvent(){
var contacts = [{
        name: 'Bob',
        lastname: 'Smith'
    },{
        name: 'James',
        lastname: 'David'
    },{
        name: 'Robert',
        lastname: 'Miller' 
    },{
        name: 'David',
        lastname: 'Martin'
    }];

    jsonexport(contacts,function(err, csv){
        if(err) return console.log(err);
        var myURL = window.URL || window.webkitURL //window.webkitURL works in Chrome and window.URL works in Firefox
        var csv = csv;  
        var blob = new Blob([csv], { type: 'text/csv' });  
        var csvUrl = myURL.createObjectURL(blob);
        setFiledownloadlink(csvUrl);
    });
}

En la función render use algo como esto:

{filedownloadlink &&<a download="UserExport.csv" href={filedownloadlink}>Download</a>}

El enlace anterior será visible cuando filedownloadlink tiene algunos datos para descargar.

Avatar de usuario de Mohamed Iqzas
Mohamed Iqzas

Agregar el siguiente código para futuras referencias. Esto es para incluir algunas comprobaciones adicionales sobre la compatibilidad del navegador y código adicional para incluir IE10+.

/**
 * Take a blob and force browser to click a link and save it from a download path
 * log out timing
 *
 * @param {Blob}
 * @method saveFile
 */
function saveFile(blob) {
    const uniqTime = new Date().getTime();
    const filename = `my_file_${uniqTime}`;

    if (navigator.msSaveBlob) { // IE 10+
        console.info('Starting call for ' + 'ie download');
        const csvFormatTimeStart = new Date().getTime();

        const ieFilename = `${filename}.csv`;
        navigator.msSaveBlob(blob, ieFilename);

        const csvFormatTimeEnd = new Date().getTime();
        const csvFormatTime = csvFormatTimeEnd - csvFormatTimeStart;
        console.log('ie download takes ' + csvFormatTime + ' ms to run');
    } else {
        console.info('Starting call for ' + 'regular download');
        const csvFormatTimeStart = new Date().getTime();
        let link = document.createElement("a");
        if (link.download !== undefined) { // feature detection
            // Browsers that support HTML5 download attribute
            var url = URL.createObjectURL(blob);
            link.setAttribute("href", url);
            link.setAttribute("download", filename);
            link.style.visibility = 'hidden';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }

        const csvFormatTimeEnd = new Date().getTime();
        const csvFormatTime = csvFormatTimeEnd - csvFormatTimeStart;
        console.log('regular download takes ' + csvFormatTime + ' ms to run');
    }

    clickEnd = new Date().getTime();
    console.log('The whole process took: ' + (clickEnd - clickStart) + ' ms');
}

El crédito debe ir a Este artículo.

avatar de usuario de morganney
morganney

Este es un componente de la función React que he usado recientemente para las descargas. Conversión bastante simple a TypeScript.

import React, { useEffect, useRef } from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'

const Link = styled.a`
  display: none;
`
const Downloader = ({ blob, filename, onDownload }) => {
  const link = useRef(null)
  const url = URL.createObjectURL(blob)

  useEffect(() => {
    link.current.click()
    onDownload()

    return () => {
      URL.revokeObjectURL(url)
    }
  }, [url, onDownload])

  return (
    <Link ref={link} href={url} download={filename}>
      Table export
    </Link>
  )
}

Downloader.propTypes = {
  blob: PropTypes.object.isRequired,
  filename: PropTypes.string.isRequired,
  onDownload: PropTypes.func.isRequired
}

export { Downloader }

Aquí hay un ejemplo rápido de cómo se puede usar:

const { useRef, useEffect, useCallback, useState } = React
const { styled } = window
const root = ReactDOM.createRoot(document.getElementById('root'))
const Link = styled.a`
  display: none;
`
const Downloader = ({ blob, filename, onDownload }) => {
  const link = useRef(null)
  const url = URL.createObjectURL(blob)

  useEffect(() => {
    console.log('download url', url)
    link.current.click()
    onDownload()

    return () => {
      URL.revokeObjectURL(url)
    }
  }, [url, onDownload])

  return (
    <Link ref={link} href={url} download={filename}>
      Table export
    </Link>
  )
}
const App = () => {
  const [download, setDownload] = useState(false)
  const [file, setFile] = useState(null)
  const handleFileChange = useCallback((evt) => {
    setFile(evt.target.files[0])
  }, [setFile])
  const handleDownload = useCallback(() => {
    setDownload(true)
  }, [setDownload])
  const onDownload = useCallback(() => {
    console.log('download finished')
    setDownload(false)
  }, [setDownload])
 
  return (
    <div>
      <form>
        <input type="file" name="some-file" onChange={handleFileChange} />
      </form>
      {file && (
        <button onClick={handleDownload}>Download file</button>
      )}
      {file && download && (
        <Downloader blob={file} filename={file.name} onDownload={onDownload} />
      )}
    </div>
  )
}

root.render(<App />)
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-is/umd/react-is.production.min.js"></script>
<script src="https://unpkg.com/styled-components/dist/styled-components.min.js"></script>
<div id="root"></div>

avatar de usuario de mattsmith5
mattsmith5

Así es como lo hago en los componentes de React Function:

const [productTemplateUrl, setProductTemplateUrl] = useState<string | undefined>(undefined,);
const downloadRef = useRef<HTMLAnchorElement>(null);

const getTemplate = async () => {
  const res = await getProductTemplate();
  const url = window.URL.createObjectURL(new Blob([res]));
  setProductTemplateUrl(url);
  if (downloadRef != null) {
    downloadRef?.current?.click();
  }
};

<a
  style={{ display: 'none' }}
  download="product-template.csv"
  href={productTemplateUrl}
  ref={downloadRef}
/>

  • Es posible que desee considerar una useEffect. Esto es algo que he usado antes: stackoverflow.com/a/74105323/258174

    – morganney

    18 de octubre a las 3:22

  • Es posible que desee considerar una useEffect. Esto es algo que he usado antes: stackoverflow.com/a/74105323/258174

    – morganney

    18 de octubre a las 3:22

¿Ha sido útil esta solución?