How to Deploy a React-Router + Flask Application

Posted by
on under

This is the third article in my "React + Flask" series, in which I discuss applications that combine a Flask API server with a React single-page application. This time I'm going to show you how to work with the popular React-Router library for React, and in particular how this library affects the production deployment of the application.

This is the third article in my "React + Flask" series. Make sure you read the first and second parts, as this part builds on the project built up to this point.

Introduction to React-Router

The React-Router library gives your React application the ability to implement client-side routes. Client-side routes can be used to change the contents of the page, or parts of it when a link is clicked. This is similar to how links allow you to navigate to different pages in a traditional application, but the navigation is entirely implemented in the client-side, without any trips to the server required.

The first step in adding React-Router to a React application is to install this library: There are two versions of the React-Router library for web and native applications. For this application you need to install the web version, which is called react-router-dom.

$ npm install react-router-dom

The React-Router library has four main components:

  • BrowserRouter: the top-level component that encloses all the routing support in your application.
  • Switch: the portion of the page that is subject to routing. The routes defined as children will be selected based on the current route.
  • Route: the definition of a client-side route.
  • Link: a link to a client-side route.

The general structure for an application that uses routing is as follows:

<BrowserRouter>
  <div>
    <!-- navigation bar with links to pages -->
    <Link to="/">Home</Link>
    <Link to="/page2">Page 2</Link>
  </div>
  <Switch>
    <Route exact path="/">
      <!-- contents for home page -->
    </Route>
    <Route path="/page2">
      <!-- contents for page 2 -->
    </Route>
  </Switch>
</BrowserRouter>

The URL matching done by React-Router is done in the order that routes are defined and is based on the start of the URL by default. For this reason, the "/" route uses the exact attribute, because if not it would match every URL. An alternative is to not use exact, but move the definition for this route to the bottom, and in that case it will act as a catch-all for any invalid client-side URLs, which sometimes is a desired feature.

If you have been following the small React + Flask project that I have been building in the previous parts of this series, you can now add a second page to it using the structure shown above. Here is the updated version of App.js:

import React, { useState, useEffect } from 'react';
import { BrowserRouter, Link, Switch, Route } from 'react-router-dom';
import logo from './logo.svg';
import './App.css';

function App() {
  const [currentTime, setCurrentTime] = useState(0);

  useEffect(() => {
    fetch('/api/time').then(res => res.json()).then(data => {
      setCurrentTime(data.time);
    });
  }, []);

  return (
    <div className="App">
      <header className="App-header">
        <BrowserRouter>
          <div>
            <Link className="App-link" to="/">Home</Link>
            &nbsp;|&nbsp;
            <Link className="App-link" to="/page2">Page2</Link>
          </div>
          <Switch>
            <Route exact path="/">
                <img src={logo} className="App-logo" alt="logo" />
                <p>
                  Edit <code>src/App.js</code> and save to reload.
                </p>
                <a
                  className="App-link"
                  href="https://reactjs.org"
                  target="_blank"
                  rel="noopener noreferrer"
                >
                  Learn React
                </a>
                <p>The current time is {currentTime}.</p>
            </Route>
            <Route path="/page2">
                <p>This is page 2!</p>
            </Route>
          </Switch>
        </BrowserRouter>
      </header>
    </div>
  );
}

export default App;

With these changes, the application adds a small navigation bar that allows you to switch between the two pages:

React-Router demo

Deployment Changes to Support Client-Side Routes

If you play with the updated application using the development web server you will notice that the URL in the navigation bar of the browser is updated as you move through pages. This is actually quite nice, as it also includes a functional back button.

Because the URL is updated as links are clicked to navigate through client-side routes, it is quite possible that at some point the user may decide to refresh the page while the URL is not the main page. If you try this while using the development server it is not a problem, but on a production deployment the refresh is going to fail with a 404 error, because the server, be it nginx or gunicorn, will receive a request for a URL such as https://example.com/page2 instead of the expected https://example.com/, and the /page2 path is not something the server knows about.

To try this, build and deploy your project using one of the methods I described in the previous part of this series. Then navigate to the second page and hit refresh.

The following sections explain how to reconfigure the server in the two deployment options I discussed in the previous part of the series to support client-side routes.

Nginx

The React application is served by the Nginx server through the following configuration block:

    location / {
        try_files $uri $uri/ =404;
        add_header Cache-Control "no-cache";
    }

You can see that in the try_files clause Nginx tries the requested path as a file first, and then as a directory. If both attempts fail, then a 404 response is configured.

The trick to make routes work is to change that final 404 error into returning the index.html page, which bootstraps the React application:

    location / {
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "no-cache";
    }

Make the above change and reload the Nginx configuration with:

$ sudo systemctl reload nginx

And now the /page2 URL (or actually any other URL, even invalid ones) will cause the React application to render. Once the application bootstraps, React-Router will look at the actual URL and apply the routing, so not only this prevents the 404 error, but also shows the correct page.

Python Web Server

If you are using a Python web server such as Gunicorn, a similar change needs to be made, so that any unknown URLs that are requested return the application's index.html.

This can be done directly in the Flask application by adding a 404 error handler. For the example project I have been building, you can add this in the api.py file:

@app.errorhandler(404)
def not_found(e):
    return app.send_static_file('index.html')

This effectively "flips" the error condition and changes it to a success request that returns the page that bootstraps the React application.

Conclusion

I have received many questions regarding client-side routing, so I hope this article addresses them. If you have any problems that I have not addressed, please let me know below in the comments!

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

15 comments
  • #1 Anand said

    Hi, Miguel.

    Thanks for the great tutorial series.

    I had a minor comment - this post suggests using npm install, while the previous post suggested yarn. If we use both side by side, we run into warnings and errors. Is it better to use yarn add react-router-dom instead?

    Thanks,
    Anand

  • #2 Miguel Grinberg said

    @Anand: I tend to use npm to install packages and yarn to run the React server. The warnings appear when you use both npm and react to install, so pick the one you like and stick to that one.

  • #3 Anand said

    @Miguel, thanks - Maybe something I did that caused both yarn and npm to be used for installs.

    As far as I remember, I followed the previous part, and it generated a yarn.lock file at some point that got pushed to heroku. When I ran npm install, it created a package-lock.json that got pushed as well. That caused conflicts and errors (having both yarn.lock and package-lock.json together). I am not sure if the yarn.lock was generated by some step in the tutorial or some way that I installed some package.

  • #4 Edgar Manukyan said

    Awesome tutorial Miguel, thank you so much!!!
    You graciously listen to our requests and build very helpful Flask + React tutorials. Much appreciated.

  • #5 Achintya Kumar said

    Hello Miguel! Great tutorial as always. I wonder since we're pointing to a function with our '/' route and not a component, would it lead to problems once the application grows or when let's say the '/' route takes a while to respond. I think I was trying something out and it leads to an internal server error due to overload. It might be unrelated so I was wondering if this could be an issue?

  • #6 Miguel Grinberg said

    @Achintya: I don't understand the problem. These routes we are discussing are all front end routes. There is no concept of a 500 error in the front end, though. The problem has to be something else, caused by an incorrect request sent to the server.

  • #7 Achintya Kumar said

    @Miguel my apologies. I'm building something where my index page sends a request to my flask backend which in turn fetches data through an external API. I also set an interval to call the '/api/data' every few seconds to update that data on my front end. I've noticed while inspecting the network that the time flask takes to respond to the get requests keeps increasing and it eventually leads to a 503 server overload. I could increase the interval time or get the data using js directly but I was wondering if there is a way to do this using flask.

  • #8 Miguel Grinberg said

    @Achintya: Why does Flask takes longer to respond each time? Isn't that really the problem that you need to debug and figure out? But in any case, sure, you can call any APIs that you like from React, that shouldn't be a problem if this external API is correctly configured for cross-origin requests.

  • #9 T P said

    Hey Miguel, very nice tutorial.

    I'm wondering how we could go about implementing CSRF (in particular using Flask-WTF) to protect this app once you start adding more complexity. Normally I would render it in a Flask template with {{ csrf_token() }} but things aren't looking so simple now that I've got a React app.

    What are your thoughts?

  • #10 Miguel Grinberg said

    @TP: that could be an interesting follow up, but if you want the quick summary, you can add an endpoint to your Flask app that returns a one line JS file that has something like var csrf_token == "{{ token }}". When the browser imports that JS it will have the token, and can add it into any requests it sends to the server.

  • #11 T P said

    @Miguel I see, my first thought was to switch the React & Flask applications around - send things through Flask first and when handling the 404 proxy to the React app.
    Much appreciated :)

  • #12 Kevin B said

    Hello Miguel, very nice Tutorial, thank you very much for your efforts!

    As a newbie to web-development I stumbled a bit over an empty page while trying to implement the react-router. After a while of researching I found out, that with the new versions 6.x of react-router-dom there is a change from p.e. <Switch> to <Routes>. So for everyone trying to follow this tutorial and wondering, check the "migration from v5 to v6" of react-router-dom.

  • #13 B.P said

    Hi Miguel

    I think this does not work in the NextJs routing. I am using below code on gunicorn:
    @app.errorhandler(404)
    def not_found(e):
    return app.send_static_file('index.html')

    but my other routes gets rediect to index.html and not to say /about or similar pages.

    Do you have any idea how to fix this problem?

    P.S: I am not using react-router-dom.

  • #14 Miguel Grinberg said

    @B.P: As you stated very well, this article is about React, not about Next.js. I haven't tested any of this with Next.js and I did not intend any of this to be usable with Next.js. I suggest you follow the Next.js documentation regarding deployment with that framework.

  • #15 Tim said

    Very helpful, thank you!

Leave a Comment