How to download fetch response in react as file
Asked Answered
A

7

76

Here is the code in actions.js

export function exportRecordToExcel(record) {
    return ({fetch}) => ({
        type: EXPORT_RECORD_TO_EXCEL,
        payload: {
            promise: fetch('/records/export', {
                credentials: 'same-origin',
                method: 'post',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(data)
            }).then(function(response) {
                return response;
            })
        }
    });
}

The returned response is an .xlsx file. I want the user to be able to save it as a file, but nothing happens. I assume the server is returning the right type of response because in the console it says

Content-Disposition:attachment; filename="report.xlsx"

What am I missing? What should I do in the reducer?

Abaft answered 4/2, 2016 at 16:38 Comment(0)
D
61

Browser technology currently doesn't support downloading a file directly from an Ajax request. The work around is to add a hidden form and submit it behind the scenes to get the browser to trigger the Save dialog.

I'm running a standard Flux implementation so I'm not sure what the exact Redux (Reducer) code should be, but the workflow I just created for a file download goes like this...

  1. I have a React component called FileDownload. All this component does is render a hidden form and then, inside componentDidMount, immediately submit the form and call it's onDownloadComplete prop.
  2. I have another React component, we'll call it Widget, with a download button/icon (many actually... one for each item in a table). Widget has corresponding action and store files. Widget imports FileDownload.
  3. Widget has two methods related to the download: handleDownload and handleDownloadComplete.
  4. Widget store has a property called downloadPath. It's set to null by default. When it's value is set to null, there is no file download in progress and the Widget component does not render the FileDownload component.
  5. Clicking the button/icon in Widget calls the handleDownload method which triggers a downloadFile action. The downloadFile action does NOT make an Ajax request. It dispatches a DOWNLOAD_FILE event to the store sending along with it the downloadPath for the file to download. The store saves the downloadPath and emits a change event.
  6. Since there is now a downloadPath, Widget will render FileDownload passing in the necessary props including downloadPath as well as the handleDownloadComplete method as the value for onDownloadComplete.
  7. When FileDownload is rendered and the form is submitted with method="GET" (POST should work too) and action={downloadPath}, the server response will now trigger the browser's Save dialog for the target download file (tested in IE 9/10, latest Firefox and Chrome).
  8. Immediately following the form submit, onDownloadComplete/handleDownloadComplete is called. This triggers another action that dispatches a DOWNLOAD_FILE event. However, this time downloadPath is set to null. The store saves the downloadPath as null and emits a change event.
  9. Since there is no longer a downloadPath the FileDownload component is not rendered in Widget and the world is a happy place.

Widget.js - partial code only

import FileDownload from './FileDownload';

export default class Widget extends Component {
    constructor(props) {
        super(props);
        this.state = widgetStore.getState().toJS();
    }

    handleDownload(data) {
        widgetActions.downloadFile(data);
    }

    handleDownloadComplete() {
        widgetActions.downloadFile();
    }

    render() {
        const downloadPath = this.state.downloadPath;

        return (

            // button/icon with click bound to this.handleDownload goes here

            {downloadPath &&
                <FileDownload
                    actionPath={downloadPath}
                    onDownloadComplete={this.handleDownloadComplete}
                />
            }
        );
    }

widgetActions.js - partial code only

export function downloadFile(data) {
    let downloadPath = null;

    if (data) {
        downloadPath = `${apiResource}/${data.fileName}`;
    }

    appDispatcher.dispatch({
        actionType: actionTypes.DOWNLOAD_FILE,
        downloadPath
    });
}

widgetStore.js - partial code only

let store = Map({
    downloadPath: null,
    isLoading: false,
    // other store properties
});

class WidgetStore extends Store {
    constructor() {
        super();
        this.dispatchToken = appDispatcher.register(action => {
            switch (action.actionType) {
                case actionTypes.DOWNLOAD_FILE:
                    store = store.merge({
                        downloadPath: action.downloadPath,
                        isLoading: !!action.downloadPath
                    });
                    this.emitChange();
                    break;

FileDownload.js
- complete, fully functional code ready for copy and paste
- React 0.14.7 with Babel 6.x ["es2015", "react", "stage-0"]
- form needs to be display: none which is what the "hidden" className is for

import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';

function getFormInputs() {
    const {queryParams} = this.props;

    if (queryParams === undefined) {
        return null;
    }

    return Object.keys(queryParams).map((name, index) => {
        return (
            <input
                key={index}
                name={name}
                type="hidden"
                value={queryParams[name]}
            />
        );
    });
}

export default class FileDownload extends Component {

    static propTypes = {
        actionPath: PropTypes.string.isRequired,
        method: PropTypes.string,
        onDownloadComplete: PropTypes.func.isRequired,
        queryParams: PropTypes.object
    };

    static defaultProps = {
        method: 'GET'
    };

    componentDidMount() {
        ReactDOM.findDOMNode(this).submit();
        this.props.onDownloadComplete();
    }

    render() {
        const {actionPath, method} = this.props;

        return (
            <form
                action={actionPath}
                className="hidden"
                method={method}
            >
                {getFormInputs.call(this)}
            </form>
        );
    }
}
Devolve answered 18/3, 2016 at 3:24 Comment(5)
@nate Can header info be packaged with this form submission?Fournier
@Fournier This is a standard HTML form submit. You can use the enctype attribute to specify three different values of the Content-Type HTTP header, but that's all. The Sending form data page on MDN might be helpful. Take a look at the section titled A special case: sending files. We have a use case where we first send an Ajax request to generate a download file, then we download. If you can use that option, you'll have more control over the headers in your Ajax request.Devolve
This example is very helpful, but it still isn't clear to me how this implementation knows about whether or not the file has been downloaded. I see that the "onDownloadComplete" is called synchronously after submit, are you just making the assumption that there aren't any errors and that the server receives the request?Dejected
@Dejected Yes, sadly, this work around does not provide a way to confirm the file download was successful. One possible solution could be to send an Ajax request prior to the download (in Widget.js) to confirm the server responds to a GET request to the download file path. Then, if successful, trigger the download. You still aren't confirming the download is successful, but if the file doesn't exist or there's some kind of network error at that time, you could handle the error. You might also want to look into putting the form in an iframe and read the iframe's content using the onload event.Devolve
This is causing me to redirect. I feel an idiot o_OEngud
A
44

You can use these two libs to download files http://danml.com/download.html https://github.com/eligrey/FileSaver.js/#filesaverjs

example

//  for FileSaver
import FileSaver from 'file-saver';
export function exportRecordToExcel(record) {
      return ({fetch}) => ({
        type: EXPORT_RECORD_TO_EXCEL,
        payload: {
          promise: fetch('/records/export', {
            credentials: 'same-origin',
            method: 'post',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify(data)
          }).then(function(response) {
            return response.blob();
          }).then(function(blob) {
            FileSaver.saveAs(blob, 'nameFile.zip');
          })
        }
      });

//  for download 
let download = require('./download.min');
export function exportRecordToExcel(record) {
      return ({fetch}) => ({
        type: EXPORT_RECORD_TO_EXCEL,
        payload: {
          promise: fetch('/records/export', {
            credentials: 'same-origin',
            method: 'post',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify(data)
          }).then(function(response) {
            return response.blob();
          }).then(function(blob) {
            download (blob);
          })
        }
      });
Affinitive answered 26/12, 2016 at 14:51 Comment(1)
thanks for sharing this. The downloadjs is excellent and perfectly solved the problem.Crucible
S
20

This worked for me.

const requestOptions = {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
};

fetch(`${url}`, requestOptions)
.then((res) => {
    return res.blob();
})
.then((blob) => {
    const href = window.URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = href;
    link.setAttribute('download', 'config.json'); //or any other extension
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
})
.catch((err) => {
    return Promise.reject({ Error: 'Something Went Wrong', err });
})
Swinney answered 7/1, 2021 at 8:46 Comment(2)
This is what I was expecting, thank you!Nominalism
Very compact solution, and it worked as intended !Puke
D
19

I have faced the same problem once too. I have solved it by creating on empty link with a ref to it like so:

linkRef = React.createRef();
render() {
    return (
        <a ref={this.linkRef}/>
    );
}

and in my fetch function i have done something like this:

fetch(/*your params*/)
    }).then(res => {
        return res.blob();
    }).then(blob => {
        const href = window.URL.createObjectURL(blob);
        const a = this.linkRef.current;
        a.download = 'Lebenslauf.pdf';
        a.href = href;
        a.click();
        a.href = '';
    }).catch(err => console.error(err));

basically i have assigned the blobs url(href) to the link, set the download attribute and enforce one click on the link. As far as i understand this is the "basic" idea of the answer provided by @Nate. I dont know if this is a good idea to do it this way... I did.

Dilorenzo answered 25/1, 2019 at 21:55 Comment(2)
Dude! U just saved my 2 days of searcing efforts... This is the answer I am looking forAnticlinorium
any idea if there is a max file size on this approach?Vanderpool
R
5

I managed to download the file generated by the rest API URL much easier with this kind of code which worked just fine on my local:

    import React, {Component} from "react";
    import {saveAs} from "file-saver";

    class MyForm extends Component {

    constructor(props) {
        super(props);
        this.handleSubmit = this.handleSubmit.bind(this);
    }

    handleSubmit(event) {
        event.preventDefault();
        const form = event.target;
        let queryParam = buildQueryParams(form.elements);

        let url = 'http://localhost:8080/...whatever?' + queryParam;

        fetch(url, {
            method: 'GET',
            headers: {
                // whatever
            },
        })
            .then(function (response) {
                    return response.blob();
                }
            )
            .then(function(blob) {
                saveAs(blob, "yourFilename.xlsx");
            })
            .catch(error => {
                //whatever
            })
    }

    render() {
        return (
            <form onSubmit={this.handleSubmit} id="whateverFormId">
                <table>
                    <tbody>
                    <tr>
                        <td>
                            <input type="text" key="myText" name="myText" id="myText"/>
                        </td>
                        <td><input key="startDate" name="from" id="startDate" type="date"/></td>
                        <td><input key="endDate" name="to" id="endDate" type="date"/></td>
                    </tr>
                    <tr>
                        <td colSpan="3" align="right">
                            <button>Export</button>
                        </td>
                    </tr>

                    </tbody>
                </table>
            </form>
        );
    }
}

function buildQueryParams(formElements) {
    let queryParam = "";

    //do code here
    
    return queryParam;
}

export default MyForm;
Raff answered 14/8, 2020 at 16:49 Comment(1)
a perfect solutionHorsefly
V
3

I think this solution is maybe a bit more "reactive" than others:

import React, { forwardRef, useImperativeHandle, useLayoutEffect, useState } from 'react';

export interface DownloadHandle {
  download: (params: { title: string; data?: Blob }) => void;
}

export const Download = forwardRef<DownloadHandle, {}>((props, ref) => {
  const linkRef = React.useRef<HTMLAnchorElement>(null);
  const [download, setDownload] = useState<{ title: string; data: Blob }>();
  useImperativeHandle(ref, () => ({
    download: (params) => {
      if (params.data) {
        setDownload(params as typeof download);
      }
    },
  }));

  //trigger download and clear data
  useLayoutEffect(() => {
    if (download) {
      linkRef?.current?.click();
    }
    setDownload(undefined);
  }, [download]);

  if (!download) {
    return null;
  }
  const { title, data } = download;
  return <a href={window.URL.createObjectURL(data)} download={title} ref={linkRef} />;
});

export type DownloadElement = React.ElementRef<typeof Download>;

Usage

const App = () => {

  const downloadRef = useRef<DownloadElement>(null);

  const handleDownload = () => {
    fetch(url, requestOptions)
    .then((res) => res.blob())
    .then((data) => {
      downloadRef.current?.download({ title: `myFile.txt`, data});
    });
  }

  return (
    <div>
      <Download ref={downloadRef} />
      <button onClick={}>Download</button>
    </div>
  )
}
Vanderpool answered 15/7, 2022 at 5:40 Comment(0)
C
2

I needed to just download a file onClick but I needed to run some logic to either fetch or compute the actual url where the file existed. I also did not want to use any anti-react imperative patterns like setting a ref and manually clicking it when I had the resource url. The declarative pattern I used was

onClick = () => {
  // do something to compute or go fetch
  // the url we need from the server
  const url = goComputeOrFetchURL();

  // window.location forces the browser to prompt the user if they want to download it
  window.location = url
}

render() {
  return (
    <Button onClick={ this.onClick } />
  );
}
Charley answered 2/10, 2019 at 14:49 Comment(2)
I think that this is the most simple and clean answer. No need to generate "fake" actions.Addis
If the url ends up generating an error response, your nice pretty error handling you probably have in your react app goes right out the window and the user's browser is filled with an ugly error.Titan

© 2022 - 2024 — McMap. All rights reserved.