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:
/business/:businessId
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} />
}
}
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.
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.
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.
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.