Refactoring React: Manage page paths through path functions

How to eliminate Shotgun Surgery in React apps by centralizing URL path construction into small, dedicated path functions.

Gustavo Santos May 4, 2022

In a React web application, it is common to have redirections between pages. It is also common to have React components that build the URL path pointing to another page outside of its context, as in the following example:

// a component used across the app

import { settingsRoute } from 'app/routes'

export const 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 it as the value to the Link’s href property.

On the other hand, the product page receives 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 creates a redirection link to the product page will need to know how to build the path to that page.

This kind of smell is called Shotgun Surgery. It happens when the same knowledge is distributed across different locations throughout the application, where each update requires changing the knowledge spread across the source code.

In 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 be updated.

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 the manual string construction by calling the productPath function, passing the product as an argument.

export const 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 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 rely 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 much easier for the reader to understand than mentally building and interpolating strings.


Original: https://dev.to/gustavofsantos/refactoring-react-manage-page-paths-through-path-functions-2dk0