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 -
-
Basic Convention:
This is the most straightforward approach, where routes are defined directly in theroutesfolder using files likeabout.tsx. It's simple and works well for small applications. -
Route Folders:
In this convention, you organize routes using folders. For example,about/route.tsxwould handle the route for/about. It allows for better structure, especially in larger apps with multiple layouts and nested routes. -
Function Based:
Here, routes are defined using functions instead of files. This approach gives you more flexible and easier to understand.
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.tsxRouting 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.tsxNote: 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?
-
Mix of Basic and Route Folder Conventions: If you feel comfortable with the first two file-based approaches, consider using a mix of both. For simpler routes, you might use the basic
about.tsxstyle. For routes with more configuration, use the folder-basedabout.$name/route.tsxstructure to keep related files organized. -
Function-Based Convention: If you find the file-based options a bit too complex or confusing, the function-based approach is the cleanest and simplest to manage overall.
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.
