Getting quickly started with React Router and vanilla React JS in 2021

How to make React Router work with a location-based state. A beginner's guide with real-world use cases and troubleshooting.

Getting quickly started with React Router and vanilla React JS in 2021

Last month I've been migrating my living Pokédex project, LivingDex , from Gatsby to a pure React JS application and I had to learn lots of things as I was rewriting the project, one of them is how React Router works.

First of all I have to say I am not a React JS expert and this article may contain mistakes or things that can be improved, but it is what worked for me so far.

Before jumping directly into learning how to manage location data with a global state like with e.g. Redux, I wanted to see how can I do it with only what React + React Router offer out of the box.

Setup

You can bootstrap a new app using Create React App or you can use an existing one. Either way, you'll have to install React Router:


npm install react-router-dom

At the moment of writing this article, I am using react-router-dom@^5.2.0.

Wrapping your application

Next thing to do would be to wrap a component with a Router component. In my case I wanted to wrap my whole app with a HashRouter component (there are many types of routers), which changes the location hash.

import React from "react"
import ReactDOM from "react-dom"
import "./index.css"
import App from "./components/App/App"
import * as serviceWorker from "./app/serviceWorker"
import { HashRouter as Router } from "react-router-dom"

ReactDOM.render(
  <React.StrictMode>
    <Router basename="/">
      <App />
    </Router>
  </React.StrictMode>,
  document.getElementById("root"),
)

// ...

This will enable us to use React Router specific hooks such as useLocation, useHistory, etc. in our App component, since we can only use them in the children of Router components, not outside.

Make sure you set up the basename attribute correctly, depending on the router type you chose.

Be aware that if you are using BrowserRouter, which changes the URL with the HTML5 History API, you may not be able to share permalinks since it may need server-side changes.

I had that problem when hosting my app in Github Pages: if you refresh a page containing a route, it will show a Github 404 error page. My solution was to use a HashRouter instead.

Render a component based on a route

In order to match a route and render a specific part of your application, you need to have a Switch component. In my case, I added it as part of my App component, because I only need one switch.

Whenever you define a Switch, you are telling the Router what to render in each path, whenever your location changes:

import { Route, Switch } from "react-router-dom"
import PokedexPage from "../pages/PokedexPage/PokedexPage"
import PokeDetailsPage from "../pages/PokeDetailsPage/PokeDetailsPage"
import BoxesPage from "../pages/BoxesPage/BoxesPage"

function App() {
  return (
    <Switch>
      <Route path="/pokedex">
        <PokedexPage />
      </Route>
      <Route path={`/pokemon/:slug`}>
        <PokeDetailsPage />
      </Route>
      <Route path="/">
        <BoxesPage />
      </Route>
    </Switch>
  )
}

export default App

As you can see, I defined many routes as /pokedex, another one with a parameter named /pokemon/:slug and at the end the home page as /.

Inside a route component, you define what will be rendered in each route.

Let's take a look at the PokeDetailsPage component:

import React from "react"
import { Link, useParams } from "react-router-dom"

function PokeDetailsPage() {
  let { slug } = useParams()

  return <div>
      <p>Pokemon: {{ slug }}</p>
      <Link to={"/pokedex"}>Go back</Link>
    </div>
}

export default PokeDetailsPage

In this page we retrieve the path parameter :slug, we render it and we display a link to the Pokedex page.

But wait, why nothing happens when we click the Go back link? The answer is that we didn't make our components dependent on the location state.

Location-aware components

One simple way to make our app to be re-rendered on location changes is to add a key attribute to the top most element that will need re-rendering. The key should represent the state of the location.

For example:

// ...
import { Route, Switch, useLocation } from "react-router-dom"

function App() {
  const loc = useLocation()
  const locationState = loc.pathname + "@" + loc.hash + "@" + loc.search.toString()

  return (
    <Switch key={locationState}>
      <Route path="/pokedex">
        <PokedexPage />
      </Route>
      {/*...*/}
    </Switch>
  )
}
// ...

In this case, every time the location hash or query string changes, a page component of the Switch will be re-rendered.

This is of course not optimal because it basically renders the whole page again, but it is a quick way to make your application aware of the location changes.

I am sure there are more efficient ways to accomplish the same. What do you think?

I hope that at least this article was useful for you to get things done and working, which was my intention.

Did you find this article valuable?

Support Javier Aguilar by becoming a sponsor. Any amount is appreciated!