A tiny wrapper library for React Router that dramatically improves type safety.
- Static types and autocompletion
- Builder API
- Zero dependencies and tiny footprint
- No code generation
- Supports lazy loading
- Pre 1.0 (expect additional features and API changes)
- React Fast Refresh is currently not supported
Jump right in with a quickstart example:
NOTE: StackBlitz doesn't display TypeScript autocomplete
Requires react-router-dom
version 6.9 or above
npm install typesafe-router
Typesafe Router splits your route definitions into 3 sections:
- Path matching fields such as
path
,index
andchildren
- Data loading/submitting fields such as
loader
andaction
- Rendering fields such as
Component
andErrorBoundary
This approach unlocks great benefits, both in terms of type safety and lazy-loading.
These definitions borrow from Matt Brophy's excellent blog post on the new React Router lazy-loading features (https://remix.run/blog/lazy-loading-routes)
// routes.ts
import { createRouteConfig } from 'typesafe-router';
const routeConfig = createRouteConfig([
{
path: '/',
children: [
{
path: 'child',
},
],
},
{
path: 'another-route',
},
] as const);
export type RouteConfig = typeof routeConfig;
NOTE: Make sure to include
as const
after the array. This requirement will be removed once TypeScript 5 is more widely adopted.
Initialise the data creator functions and add the React Router utils that will be used in your actions and loaders
// utils.ts
import { initDataCreators } from 'typesafe-router';
import { redirect } from 'react-router-dom';
import type { RouteConfig } from './routes';
export const { createAction, createLoader } =
initDataCreators<RouteConfig>().addUtils({ redirect });
// exampleRoute.tsx
import { createAction, createLoader } from './utils';
export const exampleAction = createAction('/', ({ params, redirect }) => {
return 'a string';
});
export const exampleLoader = createLoader('/', ({ params, redirect }) => {
return 123;
});
NOTE: The first argument is the route ID. Route IDs are automatically generated from your paths, with '_' segments added for pathless routes and '_index' segments added for index routes. You can also provide your own IDs in the route config if you prefer.
Create the data config by adding your actions and loaders to the route config and then export the type
// routes.ts
/* existing imports */
import { exampleAction, exampleLoader } from './exampleRoute';
/* existing code */
const dataConfig = routeConfig
.addActions(exampleAction)
.addLoaders(exampleLoader);
export type DataConfig = typeof dataConfig;
Initialise the render creator functions and add the React Router utils that will be used in your components and error boundaries
// utils.ts
/* existing imports */
import { initRenderCreators } from 'typesafe-router';
import {
Link,
useActionData,
useLoaderData,
useParams,
} from 'react-router-dom';
import type { DataConfig } from './routes';
/* existing code */
export const { createComponent, createErrorBoundary } =
initRenderCreators<DataConfig>().addUtils({
Link,
useActionData,
useLoaderData,
useParams,
});
// exampleRoute.tsx
/* existing imports */
import { createComponent, createErrorBoundary } from './utils';
export const exampleComponent = createComponent(
'/',
({ useLoaderData, Link }) =>
() => {
const loaderData = useLoaderData();
return (
<>
<p>The data is: {loaderData}</p>
<Link to="/another-route">Link</Link>
</>
);
}
);
export const exampleErrorBoundary = createErrorBoundary('/', () => () => {
return <p>Error text</p>;
});
Create the routes by adding the components and error boundaries to the data config and calling toRoutes()
// routes.ts
/* existing imports */
import { exampleComponent, exampleErrorBoundary } from './exampleRoute';
/* existing code */
export const routes = dataConfig
.addComponents(exampleComponent)
.addErrorBoundaries(exampleErrorBoundary)
.toRoutes();
// main.tsx
import { routes } from './routes';
import { createBrowserRouter } from 'react-router-dom';
const router = createBrowserRouter(routes);
// another-route.tsx
import { createComponent } from './utils';
export const Component = createComponent('/another-route', () => () => {
return <h2>Lazy-loaded route component</h2>;
});
// routes.ts
/* existing imports */
import { lazy } from 'typesafe-router';
/* existing code */
export const routes = dataConfig
.addComponents(
exampleComponent,
lazy('/another-route', () => import('./another-route'))
)
.addErrorBoundaries(exampleErrorBoundary)
.toRoutes();
TIP:
lazy
can also be used to import loaders, actions and error boundaries. If you are combining lazy and static imports (e.g. a static loader and a lazy component) make sure they are in different files.
Typesafe Router currently supports the following React Router utils.
- redirect
- Form (initial support)
- Link
- NavLink
- Navigate
- useActionData
- useLoaderData
- useNavigate
- useParams
- useRouteLoaderData
- useSubmit (initial support)
- more coming soon...
This guide will highlight any differences from their React Router equivalents.
NOTE: Paths are defined as string literals and the possible relative and absolute values are inferred. If the path includes dynamic segments then these are required and defined in a
params
object. AsearchParams
object and/orhash
string can also be defined. For components (<Link>
etc.) these objects are provided as props. For hooks (useNavigate
etc.) they are provided as properties of the second argument.
The path is inferred (see note above).
The path is inferred (see note above)
The path is inferred (see note above)
The path is inferred (see note above)
The return type is inferred from the action function defined on the route.
The return type is inferred from the loader function defined on the route.
The path is inferred (see note above).
The params are inferred. The type is the union of all the possible combinations for the current route. Params set higher in the route hierarchy are always available whereas params set lower can be accessed by narrowing the type.
The return type is inferred from the loader function with the given ID. IDs are restricted to routes that have loaders and could be rendered at the same time as the current route. If the loader is lower in the route hierarchy then the return type is a union with undefined.