24

I am building an application with a ReactJS front end that connects to an Express API server. Calls to the API are made using Ajax.

In one of my views, a table loads with "Export" links on each row. The Export links lead to a React route that calls an API endpoint which provides a CSV file to download.

If I hit the API endpoint directly with a valid request (outside the React app), a file download is initiated in my browser. Perfect! However, following the Export link from the React page attempts to load the view where the call to the API occurs. The table disappears from the view and is replaced by the file contents (on purpose to prove I have the data) but no file is downloaded.

Can I force a download of the contents of the response object as a file? Could this take place in the ajax success callback? I made an attempt with javascript but I'm struggling with the React virtual DOM... I assume this must be pretty straight forward but I'm stumped.

EDIT: Comments by @Blex helped me solve this issue! The solution is added to the code snippet...

Here is the JSX that receives the data:

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>
            )
    }
});
fryeguy
  • 875
  • 1
  • 9
  • 16
  • 1
    You can do it this way: http://jsfiddle.net/8f2ah406/ The only issue here is the file name and extension need to be set manually when prompted. Otherwise, just `window.open(/* URL of the Ajax request */);` – blex Jul 03 '15 at 22:12
  • 1
    That worked perfectly! Now I just need to figure out how to set the filename auotmagically. – fryeguy Jul 03 '15 at 22:22
  • Related: http://stackoverflow.com/questions/14964035/how-to-export-javascript-array-info-to-csv-on-client-side/14966131#14966131 – Peeyush Kushwaha Jul 03 '15 at 22:24
  • 1
    There you go: http://jsfiddle.net/unmf5dp0/ – blex Jul 03 '15 at 22:28
  • Thanks @PeeyushKushwaha for the link. In regular javascript I think this works well. However I am working in a React JS environment the abstracts the javascript DOM away from the user. The concept of a virtual DOM is still pretty foreign to me and makes many tasks complicated. – fryeguy Jul 04 '15 at 03:59
  • I posted a bounty, whats the updated answer for latest version of react?? I am using typescript – mattsmith5 Oct 04 '22 at 00:16
  • @mattsmith5 are you having any specific issues? – acdcjunior Oct 14 '22 at 19:06
  • hi @acdcjunior nah, just wanted to validate my code, it works, if anyone has better ideas for React functional components in 2022, thanks – mattsmith5 Oct 14 '22 at 19:11
  • @mattsmith5 I've added another example of a React function component. – morganney Oct 20 '22 at 13:41

5 Answers5

39

Adding the following code based on comments by @blex got the file download working. To see it in context, take a look at the success callback in the question.

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();
fryeguy
  • 875
  • 1
  • 9
  • 16
  • 1
    If you append tempLink to the body it will work in Firefox too. Not sure about Safari. – jstice4all Oct 03 '18 at 15:38
  • I wouldn't use this approch atleast for the Reactjs because it will re genrate entire dom tree. – Mahesh Sep 12 '19 at 05:00
  • 1
    add `var` for `tempLink` variable – sharun k k Jan 19 '21 at 12:16
  • Is this method going to handle a file of any kind of real size? I mean, creating an object as a URL? That really going to handle a 4GB file? – DiggyJohn May 12 '21 at 19:14
  • The original post was close to six years ago, but if I remember correctly, the point of what I was trying to accomplish was to provide an option for downloading the data displayed on the page in CSV format. I suppose if I could view the data on the page, I see no reason a large file would fail. However, I'm doubtful if any of the data I was working with rose beyond several MB let alone a GB. Maybe try a test case. – fryeguy May 19 '21 at 02:21
  • I posted a bounty, whats the updated answer for latest version of react?? – mattsmith5 Oct 04 '22 at 00:16
2

I used a package jsonexport in my React app and now I am able to download the csv file on a link click. Here is what I did:

.
.
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

.
.
.

Create a function that will provide data for CSV. It can also be in a callback from a network request. When this method is called, it will set value in filedownloadlink state.

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

In the render function use something like this:

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

The above link will be visible when filedownloadlink has some data to download.

Neo
  • 366
  • 5
  • 15
1

Adding the below code for future reference. This is to include some additional checks on browser compatibility and additional code to include 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');
}

Credit should go to this article.

Aaron Meese
  • 1,670
  • 3
  • 22
  • 32
Mohamed Iqzas
  • 976
  • 1
  • 14
  • 19
1

This is how I do it in React Function components:

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}
/>
mattsmith5
  • 540
  • 4
  • 29
  • 67
-1

This is a React function component I've used recently for downloads. Pretty simple conversion to 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 }

Here is a quick example of how it can be used:

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>
morganney
  • 6,566
  • 1
  • 24
  • 35