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.

Tokens
13.5K
Snippets
67
Records
71
Agent score
57%

What's inside Type Route

  1. Overview of Type Route TypeScript support

    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.
  2. 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.
  3. Define route parameters using the param object

    The param object is used within defineRoute to 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`
      })
    });
  4. Example: Creating a nested route hierarchy using .extend

    This example demonstrates how to define a base user route with a userId parameter and then extend it to create specific sub-routes like userSummary, userSettings, and userPost. The userPost route adds its own postId parameter, which is merged with the base userId parameter.
    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
      ),
    });
  5. Prevent and retry application route changes using session.block

    Use the session.block function to create a navigation blocker. When a route change is attempted, the blocker function is executed and receives an update object 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();
      }
    });
  6. 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 push to add a new entry to the application's navigation history (allowing the user to go back to the previous page).
    • Use replace to overwrite the current entry in the navigation history (preventing the user from returning to the previous page via the back button).
  7. Display and switch components based on the current route

    Use the useRoute() hook to access the current route's state. The route.name property can be used to conditionally render components. Because Type Route is designed for TypeScript, the route object 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>;
    }
  8. Implement a custom QueryStringSerializer

    For edge cases requiring full control over query string construction, you can provide a custom queryStringSerializer in the RouterOpts. The serializer must implement parse (to convert a raw query string into a key-value map) and stringify (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`),
    });
  9. 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 with optional to 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}`
    );
  10. Define nested routes using RouteDefinition.extend

    To avoid repeating base paths and parameters in nested routing structures, use the extend method on a RouteDefinition object returned by defineRoute. 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"),
    });
  11. Implement custom link behavior in Type Route

    While Type Route provides a standard .link property for anchors, you can implement custom link logic (such as adding analytics or integrating with React's useTransition) by manually using the href and push() method of a route. To maintain standard browser behavior (like allowing Ctrl+Click to open in a new tab), use the preventDefaultClickBehavior utility 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>
  12. Implement Server Side Rendering with Type Route

    To support Server Side Rendering (SSR) in Type Route, use the router.session.reset function on the server. This reconfigures the underlying history session to use a memory type 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);