Remix's routing system is one of its most powerful features. Built on React Router v6, it provides a clean, intuitive way to manage nested layouts, data loading, and URL structure. This comprehensive guide covers everything you need to master routing in Remix.
Route File Conventions
Remix uses file-based routing with specific naming conventions:
Basic Routes
app/routes/
├── _index.tsx # / (index)
├── about.tsx # /about
├── blog.tsx # /blog
├── blog._index.tsx # /blog (parent + index)
└── contact.tsx # /contactDynamic Segments
app/routes/
├── posts.$postId.tsx # /posts/:postId
├── users.$username.tsx # /users/:username
├── files.$.tsx # Wildcard route (/files/*)
└── api.v1.$.tsx # /api/v1/*Nested Routes
app/routes/
├── dashboard.tsx # /dashboard
├── dashboard._index.tsx # /dashboard
├── dashboard.settings.tsx # /dashboard/settings
├── dashboard.analytics.tsx # /dashboard/analytics
└── dashboard.users.tsx # /dashboard/usersSplats and Optional Segments
// app/routes/files.$.tsx
export default function Files() {
return <div>File explorer</div>;
}
// app/routes.docs.$.tsx
// Matches /docs, /docs/react, /docs/react/hooks, etc.Nested Routing
Nested routes allow you to share layouts and load data hierarchically:
// app/routes/dashboard.tsx
import { Outlet, Link, useLocation } from '@remix-run/react';
export default function DashboardLayout() {
return (
<div className="dashboard">
<aside>
<nav>
<Link to="/dashboard/overview">Overview</Link>
<Link to="/dashboard/users">Users</Link>
<Link to="/dashboard/settings">Settings</Link>
</nav>
</aside>
<main>
<Outlet /> {/* Child routes render here */}
</main>
</div>
);
}// app/routes/dashboard.users.tsx
export default function Users() {
return (
<div>
<h2>Users</h2>
<UserList />
</div>
);
}Route Hierarchy and Data
Each route can have its own loader:
// app/routes/products.tsx
import { Outlet, useLoaderData } from '@remix-run/react';
import { json } from '@remix-run/node';
export const loader = async () => {
const categories = await getCategories();
return json({ categories });
};
export default function Products() {
const { categories } = useLoaderData<typeof loader>();
return (
<div>
<nav>
{categories.map(cat => (
<Link key={cat.id} to={`/products/${cat.slug}`}>
{cat.name}
</Link>
))}
</nav>
<Outlet />
</div>
);
}// app/routes/products.$category.tsx
export const loader = async ({ params }: LoaderArgs) => {
const products = await getProductsByCategory(params.category);
return json({ products });
};
export default function CategoryProducts() {
const { products } = useLoaderData<typeof loader>();
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}Dynamic Routes
Single Dynamic Segment
// app/routes/posts.$postId.tsx
import { useLoaderData, useParams } from '@remix-run/react';
export const loader = async ({ params }: LoaderArgs) => {
const post = await getPost(params.postId);
if (!post) {
throw new Response('Not Found', { status: 404 });
}
return json(post);
};
export default function Post() {
const post = useLoaderData<typeof loader>();
const params = useParams();
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}Multiple Dynamic Segments
// app/routes.orgs.$orgId.projects.$projectId.tsx
export const loader = async ({ params }: LoaderArgs) => {
const { orgId, projectId } = params;
const project = await getProject(orgId, projectId);
return json(project);
};
export default function ProjectDetails() {
const project = useLoaderData<typeof loader>();
return (
<div>
<h1>{project.name}</h1>
<p>Organization: {project.orgId}</p>
</div>
);
}Optional Segments
// app/routes.lang-$.tsx
// Matches /lang-en, /lang-es, etc.
export const loader = async ({ params, request }: LoaderArgs) => {
const lang = params['*']?.replace('lang-', '') || 'en';
// Use language preference
};
export default function Lang() {
return <div>Language page</div>;
}Route Configuration
Custom Route Paths
// app/routes/admin.tsx
import { redirect } from '@remix-run/node';
export const loader = async () => {
// Check authentication
const user = await getUser(request);
if (!user?.isAdmin) {
return redirect('/login');
}
return json({ user });
};
export default function Admin() {
return (
<div>
<h1>Admin Dashboard</h1>
</div>
);
}Route Meta
import type { MetaFunction } from '@remix-run/node';
export const meta: MetaFunction = () => {
return [
{ title: 'My Awesome App' },
{ name: 'description', content: 'Welcome to my app' },
];
};
export default function Route() {
return <div>Hello</div>;
}Dynamic Meta
export const meta: MetaFunction<typeof loader> = ({ data }) => {
return [
{ title: data?.post.title || 'Post' },
{ name: 'description', content: data?.post.excerpt },
];
};Links Function
The links function defines CSS links:
import styles from '~/styles/dashboard.css?url';
export function links() {
return [
{ rel: 'stylesheet', href: styles },
{
rel: 'stylesheet',
href: 'https://cdn.example.com/external.css',
},
];
}
export default function Dashboard() {
return <div>Dashboard</div>;
}Route Modules
A complete route module with all exports:
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import { Link, Outlet, useLoaderData } from '@remix-run/react';
import type { MetaFunction, LinksFunction } from '@remix-run/node';
import styles from '~/styles/route.css?url';
// Meta
export const meta: MetaFunction = () => {
return [{ title: 'My Route' }];
};
// Links
export const links: LinksFunction = () => {
return [{ rel: 'stylesheet', href: styles }];
};
// Loader
export const loader = async ({ request }: LoaderFunctionArgs) => {
const data = await fetchData(request);
return json(data);
};
// Action
export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData();
await processFormData(formData);
return redirect('/success');
};
// Error Boundary
export function ErrorBoundary() {
return <div>Something went wrong</div>;
}
// Component
export default function Route() {
const data = useLoaderData<typeof loader>();
return (
<div>
<h1>My Route</h1>
<Outlet />
</div>
);
}Parallel Loading
Remix loads nested routes in parallel:
// app/routes/checkout.tsx
export const loader = async () => {
const cart = await getCart();
const user = await getUser();
return json({ cart, user });
};
// app/routes/checkout.shipping.tsx
export const loader = async () => {
const shippingMethods = await getShippingMethods();
return json({ shippingMethods });
};
// app/routes/checkout.payment.tsx
export const loader = async () => {
const paymentMethods = await getPaymentMethods();
return json({ paymentMethods });
};All three loaders run in parallel for maximum performance.
Route Protection
Authentication Required
// app/routes/dashboard.tsx
import { redirect } from '@remix-run/node';
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await getUser(request);
if (!user) {
return redirect('/login');
}
return json({ user });
};
export default function Dashboard() {
const { user } = useLoaderData<typeof loader>();
return <div>Welcome, {user.name}</div>;
}Role-Based Access
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await getUser(request);
if (!user?.roles.includes('admin')) {
throw new Response('Forbidden', { status: 403 });
}
return json({ user });
};Pagination
// app/routes/products.tsx
export const loader = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const page = Number(url.searchParams.get('page') || '1');
const limit = 20;
const { products, total } = await getProducts({ page, limit });
const totalPages = Math.ceil(total / limit);
return json({ products, page, totalPages });
};
export default function Products() {
const { products, page, totalPages } = useLoaderData<typeof loader>();
return (
<div>
<ProductList products={products} />
<div className="pagination">
{page > 1 && (
<Link to={`?page=${page - 1}`}>Previous</Link>
)}
<span>Page {page} of {totalPages}</span>
{page < totalPages && (
<Link to={`?page=${page + 1}`}>Next</Link>
)}
</div>
</div>
);
}Conclusion
Remix's routing system provides a powerful, intuitive way to manage application structure. By understanding file conventions, nested routes, and route modules, you can build complex applications with clean, maintainable code.
The key is to think in terms of layouts and data hierarchy. Each route can load its own data, define its own meta, and handle its own errors. This leads to better code organization and improved performance through parallel data loading.