Type Route Documentation
website·Indexed 32 days ago
https://type-route.zilch.dev/A type-safe routing library featuring excellent React integration, though it can be used without React. It provides utilities for programmatic navigation, server-side rendering, code splitting, and complex route parameters, along with a TypeScript-specific API for route definitions and narrowing.
What's inside Type Route
- Type Route is designed with a 'Type Safe' first approach, focusing on excellent TypeScript support. Its API is specifically engineered for static analysis, which allows the TypeScript compiler to provide validation and enables IDEs to offer intelligent hints and warnings during development.
Data fetching strategy in Type Route
Type Route is unopinionated regarding data fetching. While standard 'fetch when a component renders' techniques work for simple use cases, they may cause 'waterfall' loading experiences where content jumps as it loads. To avoid this, it is recommended to explore React Suspense patterns or handle data loading at the route level rather than the component level to trigger network requests before rendering.Define route parameters using the param object
Theparamobject is used withindefineRouteto declare the types and positions of parameters a route accepts. Parameters are defined by combining a position, optional modifiers, and a type.import { createRouter, defineRoute, param } from "type-route"; export const { routes } = createRouter({ user: defineRoute( { userId: param.path.string }, p => `/user/${p.userId}` ), userList: defineRoute({ { page: param.query.optional.number.default(1), search: param.query.optional.string }, () => `/user` }) });Example: Creating a nested route hierarchy using .extend
This example demonstrates how to define a baseuserroute with auserIdparameter and then extend it to create specific sub-routes likeuserSummary,userSettings, anduserPost. TheuserPostroute adds its ownpostIdparameter, which is merged with the baseuserIdparameter.const user = defineRoute( { userId: param.path.string, }, (p) => `/user/${p.userId}` ); const { routes, listen } = createRouter({ home: defineRoute("/"), userSummary: user.extend("/"), // Matches /user/:userId/ userSettings: user.extend("/settings"), // Matches /user/:userId/settings userPostList: user.extend("/post"), // Matches /user/:userId/post userPost: user.extend( { postId: param.path.string, }, (p) => `/post/${p.postId}` // Matches /user/:userId/post/:postId ), });Prevent and retry application route changes using session.block
Use the
session.blockfunction to create a navigation blocker. When a route change is attempted, the blocker function is executed and receives anupdateobject containing:route: The destination route of the attempted navigation.retry: A function to re-trigger the navigation attempt (typically called after the blocker is removed).
Note: While internal application routes can be blocked, navigation away from the application (e.g., closing the tab, reloading the page, or visiting an external URL) cannot be fully prevented; the browser will instead show a generic confirmation dialog if a blocker is active.
const unblock = session.block((update) => { if (confirm("Are you sure?")) { unblock(); update.retry(); } });Trigger programmatic navigation using push and replace
Programmatic navigation allows you to change the current route via code, which is useful for scenarios like redirecting old URLs or forcing a user to a login page.
- Use
pushto add a new entry to the application's navigation history (allowing the user to go back to the previous page). - Use
replaceto overwrite the current entry in the navigation history (preventing the user from returning to the previous page via the back button).
- Use
Display and switch components based on the current route
Use theuseRoute()hook to access the current route's state. Theroute.nameproperty can be used to conditionally render components. Because Type Route is designed for TypeScript, therouteobject is automatically typed based on the route name, providing type-safe access to parameters.import React from "react"; import { useRoute, routes } from "./router"; import type { Route } from "type-route"; export function Page() { const route = useRoute(); return ( <> {route.name === "home" && <HomePage />} {route.name === "userList" && <UserListPage route={route} />} {route.name === "user" && <UserPage route={route} />} {route.name === false && "Not Found"} </> ); } function UserPage({ route }: { route: Route<typeof routes.user> }) { // route.params.userId is type-safe here return <div>User: {route.params.userId}</div>; }Implement a custom QueryStringSerializer
For edge cases requiring full control over query string construction, you can provide a customqueryStringSerializerin theRouterOpts. The serializer must implementparse(to convert a raw query string into a key-value map) andstringify(to convert a map of parameters back into a query string). This acts as a mapping layer before individual parameter serializers process the values.import { createRouter, defineRoute, param, RouterOpts, QueryStringSerializer, } from "type-route"; const queryStringSerializer: QueryStringSerializer = { parse: (raw) => { const queryParams: Record<string, string> = {}; for (const part of raw.split("&")) { const [rawParamName, rawParamValue, ...rest] = part.split("="); if (rawParamName === undefined || rawParamValue === undefined || rest.length > 0) continue; const key = decodeURIComponent(rawParamName); queryParams[key] = rawParamValue; } return queryParams; }, stringify: (queryParams) => { return Object.keys(queryParams) .map((name) => { const encodedName = encodeURIComponent(name); const value = queryParams[name].value; return `${encodedName}=${value}`; }) .join("&"); }, }; const config: RouterOpts = { queryStringSerializer }; const { routes } = createRouter(config, { example: defineRoute({ arrayValue: param.query.array.string }, () => `/example`), });Apply parameter modifiers (Optional, Default, Array, Trailing)
Modifiers change the behavior and requirements of a parameter:
optional: Allows the route to match even if the parameter is missing. For path parameters, this is only allowed at the end of the path.default(value): Used withoptionalto provide a fallback value if none is provided.array: Converts the parameter type into an array of that type.trailing: Only for path parameters. Acts as a wildcard/catch-all and must be the last parameter in the path.
defineRoute( { // Optional query param with default page: param.query.optional.number.default(1), // Optional array of numbers selectedUserIds: param.query.optional.array.number, // Trailing wildcard path param slug: param.path.trailing.optional.string, }, (p) => `/foo/${p.slug}` );Define nested routes using RouteDefinition.extend
To avoid repeating base paths and parameters in nested routing structures, use theextendmethod on aRouteDefinitionobject returned bydefineRoute. This allows you to create child routes that inherit the parameters and base path of a parent route.import { createRouter, defineRoute, param } from "type-route"; // Define the parent route const user = defineRoute( { userId: param.path.string, }, (p) => `/users/${p.userId}` ); const { routes } = createRouter({ home: defineRoute("/"), about: defineRoute("/about"), user, // Extend the parent route to create nested paths userSettings: user.extend("/settings"), userActivity: user.extend("/activity"), });Implement custom link behavior in Type Route
While Type Route provides a standard.linkproperty for anchors, you can implement custom link logic (such as adding analytics or integrating with React'suseTransition) by manually using thehrefandpush()method of a route. To maintain standard browser behavior (like allowing Ctrl+Click to open in a new tab), use thepreventDefaultClickBehaviorutility to determine if the click event should be intercepted and handled by the router.import { Route, preventDefaultClickBehavior } from "type-route"; import { routes } from "./router"; // Example as a helper function function link(to: Route<typeof routes>) { return { href: to.href, onClick: (e: React.MouseEvent) => { if (preventDefaultClickBehavior(e)) { to.push(); } }, }; } // Usage <a {...link(routes.foo({ bar: "abc" }))}>Foo</a>Implement Server Side Rendering with Type Route
To support Server Side Rendering (SSR) in Type Route, use therouter.session.resetfunction on the server. This reconfigures the underlying history session to use amemorytype and initializes it with the current request URL, ensuring the server renders the correct route before sending the HTML to the client.import fastify from "fastify"; import ReactDOM from "react-dom/server"; import React from "react"; import { App, session, RouteProvider } from "../client"; const app = fastify(); app.get("/*", (request, response) => { // Reset session to memory type with the current request URL for SSR session.reset({ type: "memory", initialEntries: [request.req.url], }); const appHtml = ReactDOM.renderToString( <RouteProvider> <App /> </RouteProvider> ); response.type("text/html"); response.send( `<body><div id="app">${appHtml}</div><script src="http://localhost/bundle.js"></script></body>` ); }); app.listen(3000);