type-route

repository·main·Indexed 19 days ago

https://github.com/zilch/type-route

A flexible, type-safe routing library built on top of the history core library with deep React integration. It provides type-safe route parameters, a RouteProvider component, and the useRoute hook for accessing route state. Features include support for path, query, and state parameters with modifiers (optional, default, array, trailing), custom value serializers via ofType, route aliasing, and route grouping for type narrowing.

Tokens
28.8K
Snippets
82
Records
93
Agent score
65%

What's inside type-route

  1. Group related routes using createGroup()

    main

    When rendering components based on active routes, checking for multiple related routes (like a parent and all its sub-routes) can become verbose. Use createGroup() to simplify this logic.

    createGroup() allows you to bundle a set of routes into a single group. You can then use the .has(route) method on the group to check if the current active route belongs to that specific group. This is particularly useful for determining which layout or sub-page component should be rendered.

    To use it:

    1. Pass an array of routes to createGroup([...]).
    2. Use group.has(route) in your conditional rendering logic.
    3. Use Route<typeof group> to type the props of your sub-page components, ensuring they accept any route within that group.
    import React from "react";
    import {
      Route,
      defineRoute,
      createRouter,
      param,
      createGroup,
    } from "type-route";
    
    const user = defineRoute(
      {
        userId: param.path.string,
      },
      (p) => `/users/${p.userId}`
    );
    
    const { routes } = createRouter({
      home: defineRoute("/"),
      about: defineRoute("/about"),
      user,
      userSettings: user.extend("/settings"),
      userActivity: user.extend("/activity"),
    });
    
    // Create a group for all user-related routes
    const groups = {
      user: createGroup([routes.user, routes.userSettings, routes.userActivity]),
    };
    
    type PageProps = {
      route: Route<typeof routes>;
    };
    
    function Page(props: PageProps) {
      const { route } = props;
    
      if (route.name === "home") {
        return <div>Home</div>;
      }
    
      if (route.name === "about") {
        return <div>About</div>;
      }
    
      // Use .has() to check if the route belongs to the user group
      if (groups.user.has(route)) {
        return <UserPage route={route} />;
      }
    
      return <div>Not Found</div>;
    }
    
    type UserPageProps = {
      route: Route<typeof groups.user>;
    };
    
    function UserPage(props: UserPageProps) {
      const { route } = props;
    
      let pageContents;
    
      if (route.name === "user") {
        pageContents = <div>Main</div>;
      } else if (route.name === "userSettings") {
        pageContents = <div>Settings</div>;
      } else if (route.name === "userActivity") {
        pageContents = <div>Activity</div>;
      }
    
      return (
        <>
          <div>User Id: {route.userId}</div>
          {pageContents}
        </>
      );
    }
  2. Use path aliases with `defineRoute`

    main

    You can provide multiple paths to a single route by passing an array of strings to defineRoute.

    • The first path in the array is treated as the primary path. This is the URL used when navigating via push, replace, etc.
    • Subsequent paths are treated as aliases.
    • If a user visits an alias, the router (when using a browser history router) will immediately redirect/change the URL to the primary path.

    This is useful for supporting legacy URLs or providing multiple entry points to the same view while maintaining a single canonical URL.

    // Static aliases: matches "/dashboard" or "/", but redirects to "/dashboard"
    defineRoute(["/dashboard", "/"]);
    
    // Parameterized aliases: matches singular or plural, but redirects to singular
    defineRoute(
      {
        userId: param.path.string,
      },
      (p) => [`/user/${p.userId}`, `/users/${p.userId}`]
    );
  3. Use the default JSON serializer with ofType

    main

    If you call ofType<SomeType>() without providing a custom ValueSerializer object, type-route will use a default JSON serializer.

    Warning: The JSON serializer only verifies that the value is valid JSON. It cannot guarantee that the parsed runtime value matches the shape of SomeType. You must handle potential type mismatches in your application logic.

  4. How createRouter works with React

    main

    The createRouter function is the central entry point for Type Route. When called, it returns an object containing several key pieces for React integration:

    • RouteProvider: A React Context Provider that must wrap your application to enable routing features.
    • useRoute: A React hook that returns the current route object, including the route name and its parsed params.
    • routes: A collection of type-safe route generators. Calling these generators allows you to construct links with correct parameters.

    Example route definitions:

    • Static route: home: defineRoute("/")
    • Query parameter route: userList: defineRoute({ page: param.query.optional.number }, () => "/user")
    • Path parameter route: `user: defineRoute({ userId: param.path.string }, (p) =>

    /user/${p.userId} )`

    const { RouteProvider, useRoute, routes } = createRouter({
      home: defineRoute("/"),
      userList: defineRoute(
        { page: param.query.optional.number },
        () => "/user"
      ),
      user: defineRoute(
        { userId: param.path.string },
        (p) => `/user/${p.userId}`
      ),
    });
  5. Define Path, Query, and State parameters

    main

    Parameters are categorized by where they appear in the URL or browser state:

    • Path Parameters (param.path): Found in the URL path (e.g., /example/abc). There can be at most one path parameter per path segment (the text between forward slashes). If you need multiple parameters within a single segment, use ofType.
    • Query Parameters (param.query): Found in the query string (e.g., ?page=1).
    • State Parameters (param.state): Found in the browser's history. These are invisible to the user and are useful for storing large or complex data that should persist during forward/back navigation.
  6. Apply parameter modifiers: optional, default, array, and trailing

    main

    Modifiers change how parameters are matched and handled:

    • optional: Ensures the route matches even if the parameter is missing. For path parameters, this can only be used if the parameter is at the end of the path.
    • default(value): Used with optional to provide a fallback value if the parameter is not provided (e.g., for pagination).
    • array: Modifies a parameter type to be treated as an array.
    • trailing: A path-only modifier used for catch-all or wildcard parameters. It must be the last parameter in a path.
    // Optional query parameter
    defineRoute(
      { page: param.query.optional.number },
      () => `/users`
    );
    
    // Default query parameter
    defineRoute(
      { page: param.query.optional.number.default(1) },
      () => `/users`
    );
    
    // Array query parameter
    defineRoute(
      { selectedUserIds: param.query.optional.array.number },
      () => `/users`
    );
    
    // Trailing (wildcard) path parameter
    defineRoute(
      { slug: param.path.trailing.optional.string },
      (p) => `/foo/${p.slug}`
    );
  7. Handle complex data types with ofType()

    main

    While type-route provides built-in support for strings, numbers, booleans, and arrays, you can use the ofType(valueSerializer) escape hatch to handle complex data types (like Date objects) in your URLs.

    To use this, you must provide a ValueSerializer that defines how to convert the data to and from a string. If the parse method cannot successfully process the input, it must return the noMatch constant to prevent the route from matching.

    Note: It is recommended to use built-in types whenever possible. Only use ofType when basic types are insufficient.

    import { createRouter, defineRoute, param, noMatch } from "type-route";
    
    // 1. Define the serializer
    const dateSerializer = {
      parse(raw) {
        const value = Date.parse(raw);
        if (isNaN(value)) {
          return noMatch;
        }
        return new Date(value);
      },
      stringify(value) {
        return value.toISOString();
      },
    };
    
    // 2. Use it in a route definition
    const { routes } = createRouter({
      example: defineRoute(
        {
          minDate: param.query.ofType(dateSerializer),
          maxDate: param.query.ofType(dateSerializer),
        },
        () => `/users`
      ),
    });
  8. Use Type Route without React using `type-route/core`

    main

    To use Type Route in a framework-agnostic way (e.g., vanilla JavaScript, Vue, Svelte), import from type-route/core instead of the main type-route package.

    When using type-route/core, the createRouter function returns a different object structure than the React version. Instead of a RouteProvider, you receive a session object which you use to manage the application lifecycle:

    • session.getInitialRoute(): Retrieves the route the application should start with.
    • session.listen(callback): Subscribes to route changes. The callback receives the nextRoute whenever the route updates.

    Important: Do not mix imports from type-route and type-route/core in the same project, as this will unnecessarily increase your bundle size by including both versions.

    import { createRouter, defineRoute, param, Route } from "type-route/core";
    
    const { routes, session } = createRouter({
      home: defineRoute("/"),
      userList: defineRoute(
        {
          page: param.query.optional.number,
        },
        () => "/users"
      ),
      user: defineRoute(
        {
          userId: param.path.string,
        },
        (p) => `/users/${p.userId}`
      ),
    });
    
    // 1. Get initial route
    renderPage(session.getInitialRoute());
    
    // 2. Listen for changes
    session.listen((nextRoute) => {
      renderPage(nextRoute);
    });
    
    function renderPage(route: Route<typeof routes>) {
      // Handle routing logic based on route.name and route.params
    }
  9. Handle unmatched routes (404) using route name

    main

    In type-route, if a URL does not match any of the routes defined in your router, the name property of the current route object will be the boolean false. You can use this check to render a 'Not Found' or 404 page within your application logic.

    When using session.getInitialRoute() or the useRoute() hook, check if route.name === false to identify an unmatched URL.

    import React from "react";
    import { createRouter, defineRoute, useRoute, RouteProvider } from "type-route";
    
    const { routes } = createRouter({
      home: defineRoute("/"),
      foo: defineRoute("/foo"),
      bar: defineRoute("/bar"),
    });
    
    function App() {
      const route = useRoute();
    
      return (
        <>
          <nav>
            <a {...routes.home().link}>Home</a>
            <a href="/path-that-does-not-match">Not Found</a>
          </nav>
    
          {/* Check for specific route names or the boolean false for 404s */}
          {route.name === "home" && <div>Home</div>}
          {route.name === "foo" && <div>Foo</div>}
          {route.name === "bar" && <div>Bar</div>}
          {route.name === false && <div>Not Found</div>}
        </>
      );
    }
    
    // Ensure App is wrapped in RouteProvider
    ReactDOM.render(
      <RouteProvider>
        <App />
      </RouteProvider>,
      document.querySelector("#root")
    );
  10. Navigate between routes using link properties

    main

    Each route function in the routes object can be called with optional parameters to generate a link object. The link object contains an href attribute and an onClick function.

    For standard SPA navigation, spread the link object directly onto an <a> tag to ensure both the URL and the click handler are applied correctly.

    import { routes } from "./router";
    
    export function Navigation() {
      return (
        <nav>
          <a {...routes.home().link}>Home</a>
          <a {...routes.userList().link}>User List</a>
          <a {...routes.userList({ page: 2 }).link}>User List Page 2</a>
          <a {...routes.user({ userId: "abc" }).link}>User "abc"</a>
        </nav>
      );
    }
  11. Use the link property for SPA navigation

    main

    In Single Page Applications (SPAs), standard <a> tags with href attributes cause full page reloads. To prevent this, you must intercept the click event and trigger a route change via the router.

    Type Route provides a link property on route objects to handle this automatically. The link property returns an object containing both href and onClick. By spreading this object onto an <a> tag, you ensure that:

    1. The onClick handler prevents the default browser reload behavior and triggers the internal route transition.
    2. The href attribute is present so the browser still treats the element as a valid link (important for SEO and accessibility).

    Always spread the entire link object to ensure both properties are applied.

    <a {...routes.fooBar().link}>Foo Bar</a>
  12. Navigate Between Routes using the link property

    main

    To create navigation links, use the routes object generated by createRouter. Calling a route function (e.g., routes.user({ userId: 'abc' })) returns an object containing a link property.

    The link property contains:

    • href: The URL string.
    • onClick: A function to handle client-side navigation.

    For standard HTML <a> tags in a Single Page Application (SPA), you should spread the link object directly onto the element to ensure both the URL is correct and the click is intercepted for client-side routing.

    import { routes } from "./router";
    
    export function Navigation() {
      return (
        <nav>
          <a {...routes.home().link}>Home</a>
          <a {...routes.userList().link}>User List</a>
          <a {...routes.userList({ page: 2 }).link}>User List Page 2</a>
          <a {...routes.user({ userId: "abc" }).link}>User "abc"</a>
        </nav>
      );
    }