Using Path Matching in React Router

Recently on a project I was working on I noticed every page was importing react-helmet in order to display the unique page title, like this:

import React from 'react'
import { Helmet } from 'react-helmet'

export const UsersPage = () => {
  return (
    <div>
      <Helmet>
        <title>TaniaCorp | Users</title>
      </Helmet>

      <h1>My Component</h1>
    </div>
  )
}

For an app that has thirty pages, that's the same code repeated thirty times, so it would be ideal to be able to define it all in one place. This aligns with the Don't Hardcode Repetitive Markup guideline in the Tao of React. Anywhere we can reduce repetition with a little clear abstraction is worth looking into.

So for routes such as a UsersPage for listing all users or UserEditorPage for a single user, you would probably have a path pattern like /users and /users/:user_id.

<PrivateRoute exact path="/users" component={UsersPage} />
<PrivateRoute exact path="/users/:user_id" component={UserEditorPage} />

I thought maybe you could just use useLocation() in the React Router package to get the path (pattern), then make an object to make /users/:user_id match to the title User Editor. However, useLocation() does not give you the pattern the route is based on - it gives you the pathname in the URL. In the case of /users/:user_id, the pathname might look like /users/123.

One way of working with this data might be to do some URL splitting or maybe write some regex, but I assumed there was a simpler way.

React Router has a function called matchPath which can be used to determine if the current path matches a pattern. Here's an example of the API for that:

const pathname = '/users/123'
const pattern = '/users/:user_id'

matchPath(pathname, { path: pattern, exact: true })

With that, you can build an map of keys (patterns) to values (titles) and use matchPath to find the current match.

import { matchPath } from 'react-router'

const pageTitles = {
  '/users': 'Users',
  '/users/:user_id': 'User Editor',
}

export const getPageTitleFromUrl = (pathname) => {
  const currentPageTitle = Object.keys(pageTitles).find((key) => {
    if (matchPath(pathname, { path: key, exact: true })) {
      return true
    }

    return false
  })

  return pageTitles[currentPageTitle]
}

Note: I'm using React Router v5 for the examples, it seems that the order of pattern and pathname have been reversed in v6 for some reason. This seems like the type of move PHP would never make.

Ultimately, you could end up pulling this information in the PrivateRoute component your application probably has.

import React from 'react'
import { Helmet } from 'react-helmet'

import { getPageTitleFromUrl } from './helpers'

const PrivateRoute = ({ component: Component, ...rest }) => {
  const location = useLocation()
  const pageTitle = getPageTitleFromUrl(location.pathname)

  // Not including any irrelevant authentication code
  return (
    <>
      <Helmet>
        <title>TaniaCorp | {pageTitle}</title>
      </Helmet>
      <Route component={Component} {...rest} />
    </>
  )
}

So what's the best part about writing this article? As I got to this point, I realized if you're using a route wrapper anyway, you could just pass the title directly into the PrivateRoute and use it as a prop instead of all the unnecessary matchPath code.

Well, sometimes the simplest solution evades us, and you never know what might seem obvious to you when you take a second look at what you've written after stepping away for a while.

That's the REAL moral of the story.

Comments