Sergio Xalambrí

Optional route segments with Remix

Let's say we have a URL structure like this:

  • /:category/products
  • /products

Both routes need to work the same way.

The first renders the list of products for a specific category. The second renders the list of products for all categories.

Because they're essentially the same route, just that the category is null in the second case, we may want to re-use the route.

And we can achieve this by re-exporting the code from one route to another!

In one of the routes, define the loader, UI, and any other code you need. Here we use the nested route to get the products if params.category exists:

// routes/$category/products.tsx
export async function loader({ params }: LoaderArgs) {
  let category = params.category;
  let products = category
    ? await getProductsByCategory(category)
    : await getAllProducts();
  return json({ products });
}

export default function Products() {
  let { products } = useLoaderData<typeof loader>();

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

Now, in the second route, re-export the exports of the first route:

// routes/products.tsx
export * from "~/routes/$category/products";
export { default } from "~/routes/$category/products";

Another option, in case we only want to re-export parts of it, we could do this:

// routes/products.tsx
export { loader, default } from "~/routes/$category/products";

With the first option, if we go to the first route and add, for example, a MetaFunction export, it will now be available in the second route as well. On the second option, we would need to add the MetaFunction re-export or export a custom one, which would let us change the meta tags, like the document title.