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, inside componentDidMount
, immediately submit the form and call it's onDownloadComplete
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
imports FileDownload
.
Widget
has two methods related to the download: handleDownload
and handleDownloadComplete
.
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.
- 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.
- 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
.
- 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).
- 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.
- 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>
);
}
}