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 I'm I missing? What should I do in the reducer?
To download a file with React. js, we can add the download attribute to an anchor element. We just add the download prop to do the download. If we don't want to use an anchor element, we can also use the file-saver package to download our 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...
FileDownload. All this component does is render a hidden form and then, inside componentDidMount, immediately submit the form and call it's onDownloadComplete prop.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.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.downloadPath, Widget will render FileDownload passing in the necessary props including downloadPath as well as the handleDownloadComplete method as the value for onDownloadComplete.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).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.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> ); } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With