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'sonDownloadCompleteprop.
- I have another React component, we'll call it Widget, with a download button/icon (many actually... one for each item in a table).Widgethas corresponding action and store files.WidgetimportsFileDownload.
- Widgethas two methods related to the download:- handleDownloadand- handleDownloadComplete.
- Widgetstore has a property called- downloadPath. It's set to- nullby default. When it's value is set to- null, there is no file download in progress and the- Widgetcomponent does not render the- FileDownloadcomponent.
- Clicking the button/icon in Widgetcalls thehandleDownloadmethod which triggers adownloadFileaction. ThedownloadFileaction does NOT make an Ajax request. It dispatches aDOWNLOAD_FILEevent to the store sending along with it thedownloadPathfor the file to download. The store saves thedownloadPathand emits a change event.
- Since there is now a downloadPath,Widgetwill renderFileDownloadpassing in the necessary props includingdownloadPathas well as thehandleDownloadCompletemethod as the value foronDownloadComplete.
- When FileDownloadis 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/handleDownloadCompleteis called. This triggers another action that dispatches aDOWNLOAD_FILEevent. However, this timedownloadPathis set tonull. The store saves thedownloadPathasnulland emits a change event.
- Since there is no longer a downloadPaththeFileDownloadcomponent is not rendered inWidgetand 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>
        );
    }
}