Heading image for post: Managing React Router Pathnames

React Javascript

Managing React Router Pathnames

Profile picture of Josh Branchaud

So, you've introduced react-router into your single page React app as a way of managing routing. The app has started to grow. There are many top-level routes for rendering different "pages". Some of those "pages" now have sub-routing within them for managing wizard workflows and sections with multiple "sub-pages". This is great. The dream of routing within your React SPA has been realized.

Fast-forward a few months, and your elegant routing solution has become a brittle, error-prone maintenance nightmare. What happened?

A Simplified Example

Early into the development of a large React SPA, I noticed the above scenario beginning to emerge. Some of what I noticed can be summarized in the following series of code snippets. Note: these code snippets are not full, working examples. Only the instructive parts have been included.

/* App.js */
return (
  <Switch>
    <Route path="/" component={Home} />
    <Route path="/blog" component={Blog} />
    <Route path="/team" component={Team} />
    <Route path="/about-us" component={AboutUs} />
  </Switch>
);

This is the top-level routing in our main app file.

/* NavLinks.js */
<nav>
  <Link to="/">Home</Link>
  <Link to="/blog">Blog</Link>
  <Link to="/team">Team</Link>
  <Link to="/about-us">About Us</Link>
</nav>

These are some navigational links that get used in a shared header.

/* Footer.js */
<div className="footer">
  <Link to="/about-us">About Us</Link>
  <Link to="/team">Team</Link>
  <Link to="/blog">Blog</Link>
</div>

Here is a shared Footer component that provides links in a different layout than the NavLinks component.

/* Blog.js */
<div className="blog">
  <h1>Our Blog</h1>
  <Switch>
    <Route path="/blog" component={BlogIndex} />
    <Route path="/blog/:id" component={BlogShow} />
  </Switch>
</div>

This is the blog part of the site that gets routed to when one of the Blog links is clicked. It has a nested Switch which handles both the index path (/blog) and a path to a specific blog post (/blog/123).

/* BlogShow */
<div className="blog-show">
  <Link to={`/blog/${props.blogId}`}><h2>{props.title}</h2></Link>

  {/* blog content ... */}

  <Link to={`/blog/${props.prevBlogId}`}>Previous Post</Link>
  <Link to={`/blog/${props.nextBlogId}`}>Next Post</Link>
</div>

This is the component that shows the content of a specific blog post. It has links throughout for itself as well as the previous and next blog posts.

The Problem

There are two separate, but related issues emerging in the above code snippets.

The first is the duplication of strings like '/blog' over and over across multiple files in the codebase. Even in this simplified example you can imagine how this could quickly get out of hand. When a string has to be typed the same way over and over, there is bound to be a time when it is mistyped. Babel cannot help with mistyped string constants.

Without a standardized way of managing pathnames, it is likely that a variety of divergent approaches to defining and referencing pathnames will result. This will become hard to maintain over time.

The second issue is that of terse, error-prone string concatenation. Everywhere you are building a parameterized pathname requires that you get all the details right. This is why many frameworks, like Rails, have URL building APIs.

The Solution

The fix to all of this is to create a single source of truth for all of your pathname needs.

First, create a routes file -- routes.js. Then move all of the string pathnames into that file, each one is its own exported constant.

export const blogPath = "/blog";

Additionally, find everywhere that parameterized pathnames are being manually concatenated together. Each of these can instead be a standard pathname building function in the routes file.

export const buildBlogShowPath = blogId => `/blog/${blogId}`;

These constants and parameterized path building functions can be imported wherever they are needed. If changes of any kind need to be applied, you are in a much more secure position to be making them. Mistyped string constants now become mistyped variable names which will be flagged by Babel.

Here is what some of the above simplified example could instead look like.

/* routes.js */
export const rootPath = "/";
export const teamPath = "/team";
export const aboutPath = "/about-us";

export const blogPath = "/blog";
export const blogShowPath = "/blog/:id";

export const buildBlogShowPath = blogId => `/blog/${blogId}`;
/* App.js */
import { rootPath, blogPath, teamPath, aboutPath } from "./routes";

<Switch>
  <Route path={rootPath} component={Home} />
  <Route path={blogPath} component={Blog} />
  <Route path={teamPath} component={Team} />
  <Route path={aboutPath} component={AboutUs} />
</Switch>
/* NavLinks.js */
import { rootPath, blogPath, teamPath, aboutPath } from "./routes";

<nav>
  <Link to={rootPath}>Home</Link>
  <Link to={blogPath}>Blog</Link>
  <Link to={teamPath}>Team</Link>
  <Link to={aboutPath}>About Us</Link>
</nav>
/* Blog.js */
import { blogPath, blogShowPath } from "./routes";

<div className="blog">
  <h1>Our Blog</h1>
  <Switch>
    <Route path={blogPath} component={BlogIndex} />
    <Route path={blogShowPath} component={BlogShow} />
  </Switch>
</div>
/* BlogShow */
import { buildBlogShowPath } from "./routes";

<div className="blog-show">
  <Link to={buildBlogShowPath(props.blogId)}><h2>{props.title}</h2></Link>

  {/* blog content ... */}

  <Link to={buildBlogShowPath(props.prevBlogId)}>Previous Post</Link>
  <Link to={buildBlogShowPath(props.nextBlogId)}>Next Post</Link>
</div>

Conclusion

There are two general principles at play in this solution. The first is the idea of reducing the number of magic strings in a codebase. The second is normalizing data so that there is a single source of truth. I've found this thinking to be well-suited to the large react-router-powered SPA on which I've been working. These, however, are broader ideas. I generally recommend looking out for opportunities to apply them. They make code more robust and resilient to change.

More posts about React Javascript react-router

  • Adobe logo
  • Barnes and noble logo
  • Aetna logo
  • Vanderbilt university logo
  • Ericsson logo

We're proud to have launched hundreds of products for clients such as LensRentals.com, Engine Yard, Verisign, ParkWhiz, and Regions Bank, to name a few.

Let's talk about your project