Decoupled navigation request handling
May 2023, by Maarten Nieber
Route management


By route management, I mean the following:

  • declaring a schema that defines routes and their parameters;
  • declaring a router component that displays a React view based on the current url;
  • exposing route functions that convert a route name and associated route parameters into a url.

In this chapter we'll look at these three aspects in detail. In the next chapter, we'll see how components can make navigation requests that result in url changes.

Declaring routes

The advantage of declaring routes separately is that we can use them in the router, but also in any other function, as we shall see later. I declare my routes in a RouteTable instance, together with a RoutesT type that contains the signature of each route. Here is the route table for the posts module:

import { RouteTable } from '/src/routes/utils/RouteTable';

export const routes = {
  post: () => '/posts/:postSlug',
  posts: () => '/posts',

export type RoutesT = {
  post: (args: { postSlug: string }) => void,
  posts: () => void,

export const getRouteTable = () => {
  const routeTable = new RouteTable();
  return routeTable;

As you can see in /src/posts/RouteTable.ts, the information about each route is specified twice. This is unfortunate, but necessary to get type safety.

In /src/routes/routeTable.ts we see how the route tables for all modules are combined into a single route table. This single route table is stored in a context and provided to all components that need it.

Now that the routes are declared we can use getRouteFns to access the so-called route functions, as shown in example.ts. A route function returns the route as a string. When we pass parameters to the route function then value interpolation is performed. When we add the route to the router (we will discuss this below) then we don't pass parameters, so that we obtain a route string that contains the parameter-names.

When we call getRouteFns we have specify the types of the routes that we want to use. This isn't perfectly type safe as we need to trust that the global route-table in fact provides these routes, but in practice this works well enough.

Declaring the routes in the router

The example code below shows how to declare a router. The PostsSwitch component uses the getRouteFns function to access the routes.

import { RoutesT as PostsRoutesT } from '/src/posts/routeTable';
import { RoutesT as FramesRoutesT } from '/src/frames/routeTable';

export const PostsSwitch = () => {
  const routes = getRouteFns<PostsRoutesT & FramesRoutesT>();

  return (
    <Route path={routes.posts()}>
      { /* Show list of posts */ }
      <PostListView className="mt-12" />
      { /* Show a particular post */ }
      <Route path={}>
        <PostView className="mt-16" />