In a React web application, it is common to have redirections between pages. And it is common to have React Components that build the URL path pointing to some other page, outside its context. Such as the following example:
// a component used across the app
import { settingsRoute } from 'app/routes'
export cons OrderDescription = () => {
const order = useOrder()
return (
<ul>
{order.products.map(product => (
<li key={product.sku}>
<Link href={`/collections/${product.collectionId}/products/${product.id}`}>
{product.name}
</Link>
</li>
)}
</ul>
)
}
In this case, the OrderDescription
component is building the path to the product page and passing as value to Link
's
href
property.
On the other hand, the product page received both the collection identifier and product identifier from the path.
// /pages/product.js
export const ProductPage = () => {
const { collectionId, productId } = useParams()
const product = useProduct(collectionId, productId)
return (
<div />
)
}
The problem here is that OrderDescription
needs to know how to build the URL path to the ProductPage
component. In fact, any page that create a redirection link to the product page will need to know how to build the path to this page.
This kind of smell is called Shotgun Surgery. It happens when the same knowledge is placed between different locations through the application, where each update requires changing the knowledge spread across the source code.
With this example, if the parameters of a Product Page need to change, every place that creates a link to a product page will have to change.
One way of dealing with this smell is by creating a class or a function that encapsulates this knowledge of building links for products.
The first step is to choose the abstraction. In this post, I'll be using a function to build the page path.
// /pages/product.js
export const productPath = product =>
`/collections/${product.collectionId}/products/${product.id}`
export const ProductPage = () => {
const { collectionId, productId } = useParams()
const product = useProduct(collectionId, productId)
return (
<div />
)
}
Now we can update every place that builds the product page path and replace them by calling the function productPath
passing the product as argument.
export cons OrderDescription = () => {
const order = useOrder()
return (
<ul>
{order.products.map(product => (
<li key={product.sku}>
<Link href={productPath(product)}>
{product.name}
</Link>
</li>
)}
</ul>
)
}
Remember to be careful and keep the tests running while making the refactoring. It's important to not make behavior changes during a refactoring. If everything is green, commit the code.
Conclusion
By using path functions, we can encapsulate the behavior of creating path links based on external parameters. We leverage on the consumer of those path parameters to describe how to build the path to that page and by doing this, we avoid knowledge leaking across the application.
Even if there is only one place that builds a reference to a page through a URL path, I'd suggest doing this refactoring because reading the function call is way easier for the reader to understand what is going on than building and interpolating strings mentally.
Top comments (0)