React Router v5 - pt 4

(Bonus) Fixing the “cannot GET url” error in React Router

You’ve been working hard on a React app that uses React Router (or Reach Router). Things are progressing nicely. You’re ahead of your deadline and you might just leave work early to enjoy those extra few hours of the weekend. You decide to do one last pass-through of your app before taking off. “That button should have a little more border-radius to it.” you think. You change it, hit refresh, your app breaks. It’s not your typical error. If it were, you wouldn’t have sworn under your breath. Cannot read property 'state' of undefined , no worries. You’ve seen that one plenty of times. This one was different. It wasn’t even that the app crashed, it’s deeper than that. You stare at your monitor through your fingers. That’s it. That’s all you get. Your whole app breaks on refresh and all you get is three words.

Cannot GET /dashboard

“Probably a hot module replacement issue. Just an anomaly” - you optimistically convince yourself. To validate your assumption you restart the app. “Home page looks good. Navigating works fine. Let’s try to refresh again.”

Cannot GET /settings

Defeat. No other words describe it so perfectly. There goes your long weekend. There might even go your whole weekend since you have literally no idea what’s going on. Luckily for you, you found this post. Meta, right?

First, let’s establish the problem you’re having. To do that, we’ll need to talk about how the browser and client-side routers work.

In the old days, things were simple. If you wanted to get the contents of /dashboard , the browser would make a GET request to your server, by inspecting the path portion of the URL the server would figure out that the user was requesting the /dashboard page. It would then grab that page and send it back to the browser as a response.

Then these things called client-side routers (CSR) came into the picture. With a CSR (like React Router), you’re no longer making requests to your server every time you change routes. Instead, your CSR is just handling that for you locally on the browser. So when you go to /dashboard , instead of making a GET request to your server, your CSR is using a browser API called history.pushState to manually change the URL and render the View for that specific route - all without causing a page refresh.

Let’s look at that process a little more in depth.

The first time a user loads your app (i.e., visits your website), they don’t have any JavaScript loaded. That means no React and no React Router - so the first request will always be to your server. Then, assuming there was a successful GET request, all your JavaScript loads and React Router confidently hijacks your routing. From here on out, any other route changes in your app will be handled by React Router.

Notice the issue yet? React Router can only load after the first successful GET request to your server (or / ). The reason for the dreaded Cannot GET /* error is because, if you’re at /dashboard and then hit refresh, the browser will make a GET request to /dashboard which will fail since you have no logic on your server for handling that request (since React Router is supposed to do it).

In case the issue is still fuzzy, here’s another example. Say you are really proud of the app you’ve been working on and you want to share it with your Mom. The app is Tic Tac Toe and has three routes, / , /play , and leaderboard . You send your Mom the link https://tictactyler.com/play since you want to play with her. When she enters that URL into her browser and hits enter, what happens? At this point, she has no JavaScript, no React, and no React Router. The browser makes a GET request to /play and, since you’re relying on React Router to handle all the routing logic (but she has no React Router yet), the app crashes and she gets Cannot GET /play .

“Alright, alright, alright.” - Matthew Mcconaughey

Now the big question, how do we fix this?

The root of the issue is that you’re relying entirely on client side routing without setting up any logic to handle server-side routing. There are two main ideas for solving this problem. The first, set up both client and server side routing. The second, redirect all server requests to /index.html which will download all the JS resources and allow React Router to take it from there. Most of the solutions we’ll look at involve the latter as it’s more simple.

Hash History

TBH, this one is kind of a hack. Have you ever seen those URLs with # in them? They’re using Hash History. The idea is by appending a # to the end of the root of your URL, anything after that # won’t be sent to the server. So if the URL was https://ui.dev/#/courses , the browser would make a GET request to https://ui.dev , get back all the JavaScript, React Router would then load, see /courses , and show the proper view for that route. React Router provides a HashRouter component you could use that will get you hash-based routing, but honestly, unless you REALLY need it, there are better options.

Catch-all

If you already have a server you’re using, this is probably your best bet. The main idea here is that you redirect all of your server requests to /index.html . The outcome is similar to Hash History. Any request that is made to your server will respond with the index page (and then fetch any JS resources you need), React Router will then take over and load the appropriate view. The actual code for this varies on which type of server you have.

Express

app.get('/*', function(req, res) {
  res.sendFile(path.join(__dirname, 'path/to/your/index.html'), function(err) {
    if (err) {
      res.status(500).send(err)
    }
  })
})

Apache .htaccess

RewriteBase /
RewriteRule ^index\.html$ - [L]
 RewriteCond %{REQUEST_FILENAME} !-f
 RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]

Nginx .conf

location / {
  if (!-e $request_filename){
    rewrite ^(.*)$ /index.html break;
  }
}

No Server

For those blessed developers who don’t have to worry about managing a server, there are options for you as well and they (typically) come baked into the hosting service you’re using. There are obviously a lot of different variations here but you need to find a service that supports client-side routers.

For example, if you host with Firebase, one of the questions it asks you is

Configure as a single-page app (rewrite all urls to /index.html)?

Netlify also supports client-side routing, you just need to create a /_redirects file with the following rule

/*  /index.html  200

As you can probably guess, that tells Netlify to redirect all requests to .index.html .

Webpack / Development

This section is for everyone who ran into this problem in development using webpack-dev-server. . Just as above, what we need to do it tell Webpack Dev Server to redirect all server requests to /index.html . There are just two properties in your webpack config you need to set to do this, publicPath and historyApiFallback .

publicPath: '/',
historyApiFallback: true,

publicPath allows you to specify the base path for all the assets within your application. historyAPIFallback will redirect 404s to /index.html .

Here’s an example of a basic webpack config file with both options in case you need it.

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './app/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index_bundle.js',
    publicPath: '/'
  },
  module: {
    rules: [
      { test: /\.(js)$/, use: 'babel-loader' },
      { test: /\.css$/, use: [ 'style-loader', 'css-loader' ]}
    ]
  },
  devServer: {
    historyApiFallback: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'app/index.html'
    })
  ]
};

That’s it. Go enjoy your weekend now :beers:.

(Bonus) Recursive paths with React Router v5

Recursive routes aren’t the most pragmatic thing in the world, but they really show off the benefits of React Router v5’s component-based approach to routing.

If you’re not familiar with nested routes, I’d check out Nested Routes with React Router v5 before continuing.

The main idea here is that since React Router v5 is just components, theoretically, you can create recursive, and therefore infinite routes. The secret lies in setting up the right data structure which can lead to the infinite routes. In this example, we’ll use an array of users who all have an id , a name , and an array of friends .

const users = [
  { id: 0, name: 'Michelle', friends: [ 1, 2, 3 ] },
  { id: 1, name: 'Sean', friends: [ 0, 3 ] },
  { id: 2, name: 'Kim', friends: [ 0, 1, 3 ], },
  { id: 3, name: 'David', friends: [ 1, 2 ] }
]

By having this data structure set up this way, when we render a Person , we’ll render all of their friends as Link s. Then, when a Link is clicked, we’ll render all of that person’s friends as Link s, and on and on. Each time a Link is clicked, the app’s pathname will become progressively longer.

Initially, we’ll be at / and the UI will look like this

Michelle's Friends

  * Sean
  * Kim
  * David

If Kim is clicked, then the URL will change to /2 (Kim’s id ) and the UI will look like this

Michelle's Friends

  * Sean
  * Kim
  * David

Kim's Friends

  * Michelle
  * Sean
  * David

If David is clicked, then the URL will change to /2/3 (Kim’s id then David’s id ) and the UI will look like this

Michelle's Friends

  * Sean
  * Kim
  * David

Kim's Friends

  * Michelle
  * Sean
  * David

David's Friends

  * Sean
  * Kim

And this process repeats for as long as the user wants to click on Link s.

Now that we have the right data structure and mental model for our app, the next thing to do it construct our initial Route s. As we just saw, we want the main kickoff point of our app to be /:id . The component that’s going to be rendered at that path (and eventually do all the heavy lifting of creating our nested Route s and Link s) is our Person component. For now, we’ll just keep it simple.

import * as React from "react"
import {
  BrowserRouter as Router,
  Route,
  Link
} from 'react-router-dom'

const users = [
  { id: 0, name: 'Michelle', friends: [1, 2, 3] },
  { id: 1, name: 'Sean', friends: [0, 3] },
  { id: 2, name: 'Kim', friends: [0, 1, 3], },
  { id: 3, name: 'David', friends: [1, 2] }
]

const Person = () => {
  return (
    <div>
      PERSON
    </div>
  )
}

export default function App() {
  return (
    <Router>
      <Route path="/:id">
        <Person />
      </Route>
    </Router>
  )
}

:computer: Play with the code.

Now one small change before we start implementing our Person component. As we just saw, the main kickoff point of our app is /:id . This is what we want, but it’s a little strange to have nothing at the main index route, / . Let’s set up a simple redirect so if the user visits / , they’ll be taken to /0 .

export default function App() {
  return (
    <Router>
      <Route exact path="/">        <Redirect to="/0" />      </Route>      <Route path="/:id">
        <Person />
      </Route>
    </Router>
  )
}

:computer: Play with the code.

Now comes the fun part, implementing our Person component.

Remember, there are a few things this component needs to be responsible for.

  1. Using the id URL parameter, it needs to find that specific person in the users array.
  2. It should render a Link for every one of that specific person’s friends.
  3. It should render a Route component which will match for the current pathname + /:id .

Let’s tackle #1. We know the id of the person we need to grab because of the URL parameter. Next, using that id , we can use Array.find to grab the person out of the users array.

const Person = () => {
  const { id } = useParams()  const person = users.find((p) => p.id === Number(id))
  return (
    <div>
      PERSON
    </div>
  )
}

:computer: Play with the code.

Next up we need to map over the person 's friends and create a Link for each one of them. The only “gotcha” here is what we pass as the to prop to Link . We want to make sure that we’re taking the current URL, however deeply nested it is, and appending the id of the person we’re mapping over to it. To grab the current URL, we can use React Router v5.1’s useRouteMatch custom Hook.

const Person = () => {
  const { id } = useParams()
  const person = users.find((p) => p.id === Number(id))
  const { url } = useRouteMatch()
  return (
    <div>
      <h3>{person.name}’s Friends</h3>      <ul>        {person.friends.map((id) => (          <li key={id}>            <Link to={`${url}/${id}`}>              {users.find((p) => p.id === id).name}            </Link>          </li>        ))}      </ul>    </div>
  )
}

[ :computer: Play with the code. ]

Finally, as stated in #3, we need to render a Route to match the pattern of our newly created Link s. Because we’re creating a nested route, similar to what we did with Link , we’ll want to make sure we append the URL parameter ( /:id ) to the app’s path up until that point. To get the path , we can use the useRouteMatch custom Hook.

const Person = () => {
  const { id } = useParams()
  const person = users.find((p) => p.id === Number(id))
  const { url, path } = useRouteMatch()
  return (
    <div>
      <h3>{person.name}’s Friends</h3>
      <ul>
        {person.friends.map((id) => (
          <li key={id}>
            <Link to={`${url}/${id}`}>
              {users.find((p) => p.id === id).name}
            </Link>
          </li>
        ))}
      </ul>

      <Route path={`${path}/:id`}>        <Person />      </Route>    </div>
  )
}

:computer: Play with the code.

That’s it. Person renders a list of Link s as well as a Route matching any of those Link s. When a Link is clicked, the Route matches which renders another Person component which renders a list of Link s and a new Route . This process continues as long as the user continues to click on any Link s.