EvolveDev
Theme toggle is loading

A Detailed Guide to Routing in Remix: Nested Routes, Layouts and Dynamic Segments

In this guide, we’ll cover the different routing methods in Remix, including file-based routing, nested routes, dynamic segments, and layouts. We’ll dive into each with detailed examples to help you understand how they work and discuss the cleanest approach to use.

Published: Nov 15, 2024

A Detailed Guide to Routing in Remix: Nested Routes, Layouts and Dynamic Segments

Introduction

Routing is a central part of building applications in Remix. There are several ways to handle routes, which can make things a bit confusing. In this guide, we’ll walk through each routing method including nested routes, dynamic segments, and file-based conventions with examples. We'll also discuss which approach keeps things the cleanest, so you can choose what works best for your project.

Note: The code examples are in TypeScript, but the approach is the same for JavaScript.

Ways of defining routes in Remix

The three main conventions for defining routes in Remix are -

Routing with Basic Convention

This is the key convention used by Remix for routing. In this section, we'll cover how to define routes using the basic convention, including layouts, basic route definitions, dynamic segments, and handling 404 pages.

Creating a basic route

In routes directory, create about.tsx.

// `routes/about.tsx`
const About = () => {
  return <div>About Page</div>;
};

Visit /about page, you'll see this component being rendered.

You can keep creating individual pages like this.

Nested Routes

We currently have about page. Let's make a /about/paul route.

Create about.paul.tsx file.

// `routes/about.paul.tsx`
const About = () => {
  return <div>About Paul</div>;
};

You can go as deep you want with this method.

However, if you go back to the /about page, you would see it's not working anymore. This is because when you create a new file like about.paul.tsx inside the routes folder, it directly overrides the /about route.

To keep both, rename the about.tsx to about._index.tsx. This means about will have an index page at /about and it can aslo have childrens like /about/paul.

Nested Routes with Layout

Currently, we have two routes i.e. /about and /about/paul. Let's make a custom layout which will be used for every route including the index.

Create about.tsx again.

// `routes/about.tsx`
import { Outlet } from "@remix-run/react";
 
const About = () => {
  return (
    <div>
      Shared Layout
      <Outlet />
    </div>
  );
};

Visit both /about and /about/paul, they should be having Shared Layout word on top of them. You can use this to make any specific design or configuration for multiple nested routes.

Dynamic Routes

To create a route with dynamic segments, do the following -

Create a about.$name.tsx.

$ sign indicates the dynamic segment. You can also chain multiple dynamic segments like about.$name.$place.tsx

Later you can get both name and place either in Loader functions or in client-side via useParams hook.

Folder Tree Overview

app/
├── routes/
   ├── _index.ts
   ├── about.tsx
   ├── about._index.tsx
   ├── about.paul.tsx
   ├── about.$name.tsx
   ├── about.$name.$place.tsx
└── root.tsx

Routing with Route Folders Convention

This is very similar to the above Basic Covention but with a more manageble format. Here, we can make folders per route which allows us to keep any route-specific components or utilities in that specifc folder.

You can also mix and match these 2 conventions.

Creating a basic route

In routes directory, create about._index/route.tsx.

// `routes/about._index/route.tsx`
const About = () => {
  return <div>About Page</div>;
};

Visit /about page, you'll see this component being rendered.

As you can see, the folder name would be the route path and it should contain a route.ts.

This is benifical as we can now put any file like utils.ts in about dir which will be specific to this route or page.

Nested Routes

Create /about.paul/route.tsx file.

// `routes/about.paul/route.tsx`
const About = () => {
  return <div>About Paul</div>;
};

Same as before, but here the folder name should be the route path.

Nested Routes with Layout

Currently, we have two routes i.e. /about and /about/paul. Let's make a custom layout which will be used for every route including the index.

First rename about folder to about._index and create about/route.tsx again.

// `routes/about/route.tsx`
import { Outlet } from "@remix-run/react";
 
const About = () => {
  return (
    <div>
      Shared Layout
      <Outlet />
    </div>
  );
};

Now, your routes under /about including the index will inherit the shared layout.

Dynamic Routes

To create a route with dynamic segments, do the following -

Create a /about.$name/route.tsx.

$ sign indicates the dynamic segment. You can also chain multiple dynamic segments like /about.$name.$place/route.tsx

Later you can get both name and place either in Loader functions or in client-side via useParams hook.

Folder Tree Overview

app/
├── routes/
   ├── _index.ts
   ├── about/
   ├── Header.tsx
   └── route.tsx
   ├── about._index/
   └── route.tsx
   ├── about.paul/
   └── route.tsx
   ├── about.$name/
   └── utils.tsx
   └── route.tsx
   ├── about.$name.$place/
   └── route.tsx
└── root.tsx

Note: Header.tsx and utils.tsx are included here to illustrate that you can organize route-specific components or utilities within their respective folders.

Routing with Function Based Convention

This is a completely different way to handle routing in Remix and follows a function based approach rather than file-based like the previous ones.

Creating a basic route

Firstly, you can delete the routes folder as we won't be using file-based appraoch anymore.

Create config.ts or a any name you want.

import { DefineRouteFunction } from "@remix-run/dev/dist/config/routes";
 
export const routesConfig = (route: DefineRouteFunction) => {
  // `Home.tsx` is inside `pages` dir.
  route("/", "pages/Home.tsx");
};

I like to keep my pages inside pages dir, you can keep them anywhere you want.

Edit vite.config.ts,

// `vite.config.ts`
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import { routesConfig } from "./app/config";
 
export default defineConfig({
  plugins: [
    remix({
      routes: (defineRoutes) => defineRoutes(routesConfig),
    }),
  ],
});

Nested Routes

Modifying the routes config inside config.ts to add nested routes.

export const routesConfig = (route: DefineRouteFunction) => {
  route("/about", "pages/About.tsx");
  route("/about/paul", "pages/About-Paul.tsx");
};

Nested Routes with Layout

Modifying the routes config inside config.ts to add nested routes with custom layout.

export const routesConfig = (route: DefineRouteFunction) => {
  // `layouts/Shared-Layout.tsx` is the component with `<Outlet />`
  route("/about", "layouts/Shared-Layout.tsx", () => {
    // Index page for /about
    route("/", "pages/About.tsx", { index: true });
    // Page for `/about/paul`
    route("/paul", "pages/About-Paul.tsx");
  });
};

Now, your routes under /about including the index will inherit the shared layout.

Dynamic Routes

Modifying the routes config inside config.ts to add dynamic routes.

export const routesConfig = (route: DefineRouteFunction) => {
  route("/about/:name", "pages/About-Profile.tsx");
  // OR
  route("/about/:name/:place", "pages/About-Profile-Place.ts");
};

Later you can get both name and place either in Loader functions or in client-side via useParams hook.

Final Routes Config Overview

export const routesConfig = (route: DefineRouteFunction) => {
  route("/about", "layouts/Shared-Layout.tsx", () => {
    route("/", "pages/About.tsx", { index: true });
    route("/paul", "pages/About-Paul.tsx");
 
    route("/about/:name", "pages/About-Profile.tsx");
    route("/about/:name/:place", "pages/About-Profile-Place.ts");
  });
};

Handling 404 Pages

All 404 pages will be caught by the ErrorBoundary in root.tsx.

Modifying root.tsx error boundary to configure 404 page.

export function ErrorBoundary() {
  const error = useRouteError();
 
  return (
    <html>
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {isRouteErrorResponse(error) && error.status === 404 ? (
          <NotFound />
        ) : (
          <ErrorPage
            // Optionally, you can pass the error.
            error={error}
          />
        )}
        <Scripts />
      </body>
    </html>
  );
}

Here, you can check if the error is of status 404 and conditionally render a <NotFound /> component.

Which Routing Convention is the Best?

More Resources

Some community-driven solutions:

For more information, visit the Remix Documentation.

Conclusion

In this guide, we covered Remix's routing conventions in detail, including nested routes and layouts, dynamic segments, and 404 handling. Each routing convention has its strengths, and the choice depends on your project's complexity and your preferences. Personally, I find the function-based approach to be the cleanest and most scalable option overall.

#remix#web-development#programming

Share on:

Copyright © EvolveDev. 2025 All Rights Reserved