How to download fetch response in react as file
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...
- I have a React component called
FileDownload
. All this component does is render a hidden form and then, insidecomponentDidMount
, immediately submit the form and call it'sonDownloadComplete
prop. - 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
importsFileDownload
. Widget
has two methods related to the download:handleDownload
andhandleDownloadComplete
.Widget
store has a property calleddownloadPath
. It's set tonull
by default. When it's value is set tonull
, there is no file download in progress and theWidget
component does not render theFileDownload
component.- Clicking the button/icon in
Widget
calls thehandleDownload
method which triggers adownloadFile
action. ThedownloadFile
action does NOT make an Ajax request. It dispatches aDOWNLOAD_FILE
event to the store sending along with it thedownloadPath
for the file to download. The store saves thedownloadPath
and emits a change event. - Since there is now a
downloadPath
,Widget
will renderFileDownload
passing in the necessary props includingdownloadPath
as well as thehandleDownloadComplete
method as the value foronDownloadComplete
. - When
FileDownload
is rendered and the form is submitted withmethod="GET"
(POST should work too) andaction={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). - Immediately following the form submit,
onDownloadComplete
/handleDownloadComplete
is called. This triggers another action that dispatches aDOWNLOAD_FILE
event. However, this timedownloadPath
is set tonull
. The store saves thedownloadPath
asnull
and emits a change event. - Since there is no longer a
downloadPath
theFileDownload
component is not rendered inWidget
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> ); }}
You can use these two libs to download files http://danml.com/download.html https://github.com/eligrey/FileSaver.js/#filesaverjs
example
// for FileSaverimport 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); }) } });
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.