Skip to main content

Route

Routes are perhaps the most important part of a ArchibaldRouter app. They couple URL segments to components and create UI layout. Through route nesting, complex application layouts and data dependencies become simple and declarative.

<Route element={<Team />} path="teams/:teamId" />

Types

/**
* Can be a wrapper for some internal Routes.
* Useful for defining a block of Routes on a shared segment.
* Also, can have a shared layout.
*
* @example
* <Route path="p" element={<ProductDetailLayout />}>
* <Route index element={<ProductDetailIndexPage />} />
* <Route path=":productId" element={<ProductDetailPage />} />
* <Route path="about" element={<ProductDetailAboutPage />} />
* </Route>
*/
type RouteWrapper = {
path: string;
children: ReactNode;
element?: ReactNode | null;
fallback?: NonNullable<ReactNode> | null;
ignoreCase?: boolean;
prefetch?: PrefetchFunction;
lazy?: RouteLazyLoader;
// not possible types
index?: never;
};

/**
* Index Route is controlled with *index* boolean prop.
* Defines what element should be rendered on the parent segment.
*
* @example
* <Route path="/:language">
* <Route index element={<HomePage />} />
* </Route>
*/
type RouteIndex = {
index: boolean;
element?: ReactNode | null;
fallback?: NonNullable<ReactNode> | null;
prefetch?: PrefetchFunction;
lazy?: RouteLazyLoader;
// not possible types
children?: never;
path?: never;
};

/**
* Common Route.
* Defines what element should be rendered on the specified path.
*
* @example
* <Route path="cart" element={<CartPage />} />
*
* @example
* <Route path="p/:productId" element={<ProductDetailPage />} />
*/
type Route = {
path: string;
element?: ReactNode | null;
fallback?: NonNullable<ReactNode> | null;
ignoreCase?: boolean;
prefetch?: PrefetchFunction;
lazy?: RouteLazyLoader;
// not possible types
children?: never;
index?: never;
};

export type RouteProps = RouteWrapper | RouteIndex | Route;

// A loader for a route-level code-split chunk, e.g. `() => import('./CartPage')`.
type RouteLazyLoader = () => Promise<{ default: ComponentType<any>; prefetch?: PrefetchFunction }>;

path

The path pattern to match against the URL to determine if this route matches a URL, link href, or form action.

Dynamic Segments

If a path segment starts with : then it becomes a "dynamic segment". When the route matches the URL, the dynamic segment will be parsed from the URL and provided as params to other router APIs.

<Route
// this path will match URLs like
// - /teams/archiabld
// - /teams/real
path="/teams/:teamId"
element={<Team />}
/>;

// and the element through `useParams`
function Team() {
const { teamId } = useParams();
console.log(params.teamId); // "archiabld"
}

You can have multiple dynamic segments in one route path:

<Route path="/:language/teams/:teamId" />;
// both will be available
params.language;
params.teamId;

Optional Segments

You can make a route segment optional by adding a ? to the end of the segment.

<Route
// this path will match URLs like
// - /categories
// - /en/categories
// - /fr/categories
path="/:lang?/categories"
element={<Categories />}
/>;

// and the element through `useParams`
function Categories() {
const { lang } = useParams();
}

You can have optional static segments, too:

<Route path="/project/task?/:taskId" />

Splats

Also known as "catchall" and "star" segments. If a route path pattern ends with /* then it will match any characters following the /, including other / characters.

<Route
// this path will match URLs like
// - /files
// - /files/one
// - /files/one/two
// - /files/one/two/three
path="/files/*"
element={<Team />}
/>;

// and the element through `useParams`
function Team() {
const params = useParams();
console.log(params['0']); // "one/two"
}

Layout Routes

Omitting the path makes this route a "layout route". It participates in UI nesting, but it does not add any segments to the URL.

<Route
element={
<div>
<h1>Layout</h1>
<Outlet />
</div>
}
>
<Route path="/" element={<h2>Home</h2>} />
<Route path="/about" element={<h2>About</h2>} />
</Route>

In this example, <h1>Layout</h1> will be rendered along with each child route's element prop, via the layout route's Outlet.

index

Determines if the route is an index route. Index routes render into their parent's Outlet at their parent's URL (like a default child route).

<Route path="/teams" element={<TeamsLayout />}>
<Route index element={<TeamsIndex />} />
<Route path=":teamId" element={<Team />} />
</Route>

element

The React Element to render when the route matches the URL.

If you want to create the React Element, use element:

<Route path="/for-sale" element={<Properties />} />

fallback

The React Element to render for a Route wrapping Suspense component while lazy-loaded element and corresponding for it data is fetched.

<Route path="/p/:productId(\d+)" element={<Product />} fallback={<ProductSkeleton />} />

prefetch

An optional function that is executed during Server-Side Rendering (SSR) when the route is matched. This allows prefetching data at the route level before the component tree is rendered, preventing loading fallbacks from appearing in the initial HTML.

On the client the same function can be triggered ahead of navigation — on hover/focus via <RouterLink prefetch> or programmatically via useRoutePrefetch — so the target page's data is already warming before the user arrives. Prefetches are deduplicated by the data client's cache.

The prefetch function receives a PrefetchContext object containing:

  • dataClient: The DataClient instance for prefetching data.
  • appClient: The AppClient instance providing application context (e.g., current language).
  • params: The URL parameters matched for this route.
  • context: The current router context.
<Route
path="p/:productId"
element={<ProductDetailPage />}
prefetch={async ({ dataClient, params, appClient }) => {
const { productId } = params;
const language = appClient.language;

await dataClient.prefetch(['product', productId, language], {
data: () => actionGetProduct(productId)
});
}}
/>

lazy

Enables route-level code splitting. Instead of importing a page component eagerly (which lands it in the entry chunk), pass a loader that dynamically imports it. The route's element is resolved from the module's default export via React.lazy, so the page's code is only downloaded when the route is first matched.

<Route path="cart" lazy={() => import('./CartPage')} fallback={<CartSkeleton />} />

The loaded module may optionally export a prefetch in addition to its default component. When the route declares no explicit prefetch prop, that module prefetch is run — and the chunk is warmed — whenever the route is prefetched (see prefetch above):

// CartPage.tsx
export default function CartPage() {
/* ... */
}

export const prefetch: PrefetchFunction = async ({ dataClient, appClient }) => {
await dataClient.prefetch(['cart', appClient.language], { data: () => actionGetCart() });
};
note

A lazy route suspends while its chunk loads, so it must be able to reach a Suspense boundary. Today that means giving the route (or an ancestor <Routes fallback>) a fallback. You can still pass an explicit element alongside lazy — the explicit element wins — and an explicit prefetch prop takes precedence over the module's prefetch.

SSR

lazy uses React.lazy, which is client-side code-splitting and is not integrated with Archibald's server-side chunk handling. On the server the chunk is not guaranteed to be available (it can fail in split serverless deployments), so lazy is only safe for routes that are never server-rendered (e.g. auth-gated, client-only pages).

For a route that can be reached by a direct/SSR request, code-split it with the app's Loadable helper (react-loadable) and pass it as element instead — the build rewrites its import() with require.resolveWeak, so the server renders the chunk synchronously and hydration stays aligned:

import Loadable from 'shop/client/components/support/loadable/Loadable';

const CartPage = Loadable({ factory: () => import('shop/client/features/cart/pages/cart') });

<Route path="cart" element={<CartPage />} fallback={<CartSkeleton />} />;

ignoreCase

Enables case-insensitive matching if set to true. If omitted or set to false, matching will be case-sensitive. As patterns are derived from the last child route, the last child route also defines if the match is case sensitive or not. ignoreCase on wrapper routes only count towards the index and does not make children case-insensitive on its own.

<Route ignoreCase path="/case/hot" />

This example matches both /case/hot and /Case/HOT or other variations.

<Route ignoreCase path="/case">
<Route index />
<Route ignoreCase path="/hot" />
<Route path="/cold" />
</Route>

In this example /case/cold is fully case-sensitive, while both /CASE and /CASE/HOT will return a match.

<Route path="/case">
<Route index />
<Route ignoreCase path="/cold" />
</Route>

Here /case will be case-sensitive while /case/cold will be case-insensitve. This means /CASE will return not found, while /CASE/COLD will return a match.