Route Parameters in React Components
Sun 01 April 2018, by Matthew Segal
Category: React

I'd like to share some small helper code that I wrote to help me with a React Router pattern that I run into quite often.

You can find the example code for this post on GitHub and view it running on GitHub pages.

So here's the problem:

There's a straightforward way to grab the "business" object you want out of the list, based on the id that is in the path:

export class BusinessDetailsContainer {
  render() {
    const {
      businesses,
      match: { params: { businessId } }, // From React Router.
    } = this.props
    // Find the right business.
    const business = businesses.find(b => b.id === businessId)
    // Return a presentational component.
    return <BusinessDetails business={business} />
  }
}

Missing Business

Cool, except sometimes the business doesn't exist, or stops existing, and we need to handle that case:

export class BusinessDetailsContainer {
  render() {
    const {
      businesses,
      match: { params: { businessId } },
    } = this.props
    const business = businesses.find(b => b.id === businessId)
    if (!business) {
      return <Redirect to="/not-found" />
    }
    return <BusinessDetails business={business} />
  }
}

This pattern works fine, but it gets unweildy as the codebase grows. What about if we have many pages which all need to grab a business based on the URL parameters? What if we want to redirect to the user's dashboard instead of a "not found" page if the business is not found? If we're implementing the same thing many times, then we should abstract it out.

Define a Wrapper

In a similar style to my previous posts, I will first define what I want from my solution before implementing it. Personally, I'd prefer some kind of wrapper function, like React Router's withRouter or Redux's connect. Some function which just injects the business into my component. Something like this:

class _BusinessDetailsContainer {
  render() {
    const { business } = this.props
    return  <BusinessDetails business={business} />
  }
}

const BusinessDetailsContainer = withBusiness(_BusinessDetailsContainer)
export { BusinessDetailsContainer }

I prefer this kind of interface, which is why I wrote it, but you could also create a context provider style API to do this as well, if that's more to your liking.

Write the Wrapper

Here's one way to do it with some global context providing the business list:

import React from 'react'
import { Redirect, withRouter } from 'react-router-dom'

import { AppContext } from './context'

// Passes a business object to the Child component.
// Returns a "not found" redirect if a business cannot be found.
const _withBusiness = Child => props => (
  <AppContext.Consumer>
    {app => {
      const { match: { params: { businessId } } } = props
      const business = app.businesses.find(b => b.id === businessId)
      return business
        ? <Child business={business} {...props} />
        : <Redirect to="/not-found" />
    }}
  </AppContext.Consumer>
)

// Inject React Router props into _withBusiness
export const withBusiness = Child => withRouter(_withBusiness(Child))

An additional benefit of this approach is that you can test this wrapper much more easily than a bunch of copy-pasted routing logic that is scattered all over your app.

Conclusion

If you find yourself copy-pasting the same code between your components, then you might want to write a wrapper function that consolidates the logic into one place. I've run into this URL parameter problem several times, so I hope you find this solution useful if you have it too.

The main downside that I've found for this approach is that the operation of your app can get a little magical. I've been bitten by this once or twice - where a page was redirecting to "not found" and I couldn't figure out why. Whacking in some verbose debug logging helps.

I'm a a web developer from Melbourne, Australia. I spend my days swinging angle brackets against the coal face, while working on Blog Reader in my spare time.