safe-routes
Type-safe helper for manipulating internal links in your React Router apps.
[!note] remix-routes has been renamed to safe-routes. If you are looking for the documentation of remix-routes, please refer to here.
Please refer to the upgrading guide if you are upgrading from remix-routes.
Highlights
Installation
$ npm add safe-routes
Setup
Add safeRoutes
plugin to your vite.config.ts
:
import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";
import { safeRoutes } from 'safe-routes/vite';
export default defineConfig({
plugins: [
reactRouter(),
safeRoutes(),
],
});
Supported config options:
strict: boolean
outDir: string
Add safe-routes typegen
to the typecheck
script:
- "typecheck": "react-router typegen && tsc --build --noEmit"
+ "typecheck": "react-router typegen && safe-routes typegen && tsc --build --noEmit"
Usage
Typed URL generation
import { redirect } from 'react-router';
import { $path } from 'safe-routes'; // <-- Import magical $path helper from safe-routes.
export const action = async ({ request }) => {
let formData = await request.formData();
const post = await createPost(formData);
return redirect($path('/posts/:id', { id: post.id })); // <-- It's type safe.
};
Appending query string
import { $path } from 'safe-routes';
$path('/posts/:id', { id: 6 }, { version: 18 }); // => /posts/6?version=18
$path('/posts', { limit: 10 }); // => /posts?limit=10
// You can pass any URLSearchParams init as param
$path('/posts/delete', [['id', 1], ['id', 2]]); // => /posts/delete?id=1&id=2
Typed query string
Define type of query string by exporting a type named SearchParams
in route file:
// app/routes/posts.tsx
export type SearchParams = {
view: 'list' | 'grid',
sort?: 'date' | 'views',
page?: number,
}
import { $path } from 'safe-routes';
// The query string is type-safe.
$path('/posts', { view: 'list', sort: 'date', page: 1 });
You can combine this feature with zod and remix-params-helper to add runtime params checking:
import { z } from "zod";
import { getSearchParams } from "remix-params-helper";
const SearchParamsSchema = z.object({
view: z.enum(["list", "grid"]),
sort: z.enum(["price", "size"]).optional(),
page: z.number().int().optional(),
})
export type SearchParams = z.infer<typeof SearchParamsSchema>;
export const loader = async (request) => {
const result = getSearchParams(request, SearchParamsSchema)
if (!result.success) {
return json(result.errors, { status: 400 })
}
const { view, sort, page } = result.data;
}
Typed route ids
safe-routes exports the RouteId
type definition with the list of all valid route ids for your repository, and has a helper function $routeId
that tells typescript to restrict the given string to one of the valid RouteId values.
import type { RouteId } from 'safe-routes';
import type { loader as postsLoader } from './_layout.tsx';
import { useRouteLoaderData } from 'react-router';
import { $routeId } from 'safe-routes';
export default function Post() {
const postList = useRouteLoaderData<typeof postsLoader>($routeId('routes/posts/_layout'));
}
Basename support
Basename is supported out of the box. If you have set a basename in your vite.config.ts
and react-router.config.ts
, safe-routes will automatically prepend the basename to the generated URLs.
// react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
basename: "/blog",
} satisfies Config;
import { $path } from 'safe-routes';
$path('/posts/:id', { id: 6 }); // => /blog/posts/6