0

I have both a React APP and a Express API server on the same server/domain. Nginx is serving the React APP and proxying the express server to /api.

Nginx configuration https://gist.github.com/dvriv/f4cff6e07fe6f0f241a9f57febd922bb

(Right now I am using the IP directly instead of a domain)

From the React APP, when the user does something I want him to download a file. I used a express route on my API server that serve the file. This works fine when the user put the URL.

This is my express route:

donwloadFile.route('/')
  .get((req, res) => {
    const file = '/tmp/PASOP180901.txt';
    res.download(file);
  });

This is my react redirect:

if (this.state.downloadFile === true) {
  this.setState({ downloadFile: false });
  setTimeout(() => {
    window.location.href = '/api/downloadFile';
  }, 100);
}

The address changes but the download don't start. If I press F5 then the download starts just fine. If I use a external URL to host my file, the download start just fine too.

Thanks

David Rivera
  • 339
  • 1
  • 4
  • 12
  • whe you say "this is my react redirect" in which function are you calling that? is it inside a onClick event or something similar? It would be useful to see the entire function call. Alternatively, try to redirect using a solution [found here](https://stackoverflow.com/questions/31079081/programmatically-navigate-using-react-router) to see if it helps – c-chavez Oct 17 '18 at 06:51
  • @c-chavez I just put it on the componentDidMount() to test it, Just loading the React Route to that component will execute that timeout and redirect. And I have already tried redirecting using React Router, and it does exactly the same – David Rivera Oct 17 '18 at 07:17

1 Answers1

0

First things first. Don't use setTimeout, but rather use the callback function of setState to execute code after the state is set ensuring it has been modified. Calling the callback function will guarantee the state is changed before that code in the callback is executed.

from the official docs:

setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses.

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied.

setState(stateChange[, callback])

The second parameter to setState() is an optional callback function that will be executed once setState is completed and the component is re-rendered. Generally we recommend using componentDidUpdate() for such logic instead.

So, instead of:

if (this.state.downloadFile === true) {
  this.setState({ downloadFile: false });
  setTimeout(() => {
    // execute code, or redirect, or whatever
  }, 100);
}

you should do:

if (this.state.downloadFile === true) {
  this.setState({ downloadFile: false }, () => {
    // execute code, or redirect, or whatever
  });
}

Now, for your specific problem

Set headers in your server side

You can set the Content-Disposition header to tell the browser to download the attachment:

from here:

In a regular HTTP response, the Content-Disposition response header is a header indicating if the content is expected to be displayed inline in the browser, that is, as a Web page or as part of a Web page, or as an attachment, that is downloaded and saved locally.

Set it like this:

('Content-Disposition: attachment; filename="/tmp/PASOP180901.txt"');

Force download from the client

There are multiple ways to force the download from the client, but this is the only one I've tried.

For this to work, you have to have the content of text somehow in the client (your express route can return it for example) and create a filename for the file that will be downloaded.

let element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
document.body.appendChild(element);
element.click();
document.body.removeChild(element);

Basically you are creating an empty link component, setting the data attribute to it with the text's content, attaching the link to the body, clicking the link and then removing the link from the body.

Open the link in a new tab

Opening the link in a new tab will trigger the download as well:

window.open('/api/downloadFile');

Redirect programatically

Have a look at this question in SO

You can do this:

this.props.history.push("/api/downloadFile")?

If cannot access this.props.history you can import { withRouter } from 'react-router-dom'; and export default withRouter(yourComponent); to access it.

c-chavez
  • 7,237
  • 5
  • 35
  • 49
  • First solution doesn't work, Pretty sure res.download already does that, The problem is that the get request to my express server is never sent. I think the redirection is used by the React app and never gets to my express server. This is because they are on the same domain or something like that. Second one does work (I found another method that does pretty much the same and it worked), so I guess I will have to use that. I wonder if is there a way to force react to send the request to my express server. Maybe it just need some configuration on the nginx config? – David Rivera Oct 18 '18 at 02:57
  • About the setState callback thanks I didn't knew about that – David Rivera Oct 18 '18 at 03:04
  • @DavidRivera what about opening a new tab with the request url `window.open('/api/downloadFile')` ? – c-chavez Oct 18 '18 at 07:38
  • @DavidRivera or trying to redirect using `this.props.history.push("/api/downloadFile")`? If you don't have `this.props.history` then you can `import { withRouter } from 'react-router-dom';` and `export default withRouter(yourComponent);` but usually it should be there. – c-chavez Oct 19 '18 at 11:05
  • push doesn't work already tried it, but opening a new tab works! :) thanks – David Rivera Oct 21 '18 at 07:31
  • update your answer and i'll choose it as the solution thanks man – David Rivera Oct 21 '18 at 07:33