npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@t8/react-router

v1.2.8

Published

Concise router for React apps

Readme

T8 React Router

Concise router for React apps

npm Lightweight CSR ✓ SSR ✓

🔹 Concise routing API

<header className={at("/", "full", "compact")}>
// at "/" ? "full" : "compact"
{at("/about", <About/>)}
// at "/about" ? <About/> : undefined
{at(/^\/sections\/(?<id>\d+)\/?$/, ({ params }) => (
  <Section id={params.id}/>
))}
// at "/sections/<id>" ? <Section id={id}/> : undefined

🔹 Familiar navigation APIs

- <a href="/about">About</a>
+ <A href="/about">About</A> // SPA route link
- window.location.assign("/about");
+ route.assign("/about"); // SPA navigation

- window.location.href = "/about";
+ route.href = "/about"; // SPA navigation

🔹 Middleware hooks

useNavigationStart(callback);
// e.g. to redirect or prevent navigation
useNavigationComplete(callback);
// e.g. to set the document's title

🔹 Typed routes and URL parameters, as an optional enhancement

   // ↓ type-safe URL pattern builder
let { url } = createURLSchema({
  "/sections/:id": z.object({ // with Zod
    params: z.object({ id: z.coerce.number() })
  })
});
                          // ↓ { id: number }
{at(url("/sections/:id"), ({ params }) => (
  <Section id={params.id}/>
))}
<A href={url("/sections/:id", { params: { id: 1 } })}>Section 1</A>
                             // ↑ { id: number }

🔹 URL parameters as state

let [state, setState] = useRouteState("/");
// with type safety based on a custom URL schema
let [state, setState] = useRouteState(url("/"));

🔹 Lazy routes

{at("/about", <Suspense><About/></Suspense>)}

🔹 SSR- and CSR-compatible

Installation: npm i @t8/react-router

Routing

The following example runs through the essential parts of routing code. The at(route, x, y) function returns a value based on whether the route parameter matches the current URL. It acts similarly to the conditional operator atRoute ? x : y and is equally applicable to components and prop values. The route link component that is used for SPA navigation acts and looks similar to the HTML link tag.

import { A, useRoute } from "@t8/react-router";
import { Intro } from "./Intro";
import { Section } from "./Section";

let App = () => {
  let { at } = useRoute();

  return (
    <>
      <header className={at("/", "full", "compact")}>
        <h1>App</h1>
        <nav>
          <A href="/">Intro</A>{" | "}
          <A href="/sections/1">Section 1</A>
        </nav>
      </header>
      {at("/", <Intro/>)}
      {at(/^\/sections\/(?<id>\d+)\/?$/, ({ params }) => (
        <Section id={params.id}/>
      ))}
    </>
  );
};

Live demo

🔹 As mentioned above, at(route, x, y) acts similarly to the ternary operator atRoute ? x : y often used with conditional rendering, which route-based rendering essentially is: it returns x if the current URL matches route, and y otherwise. Having the ternary function rather than the ternary conditional operator allows for additional flexibility, like omitting an undefined fallback parameter or resolving as a dynamic value based on params extracted from the route pattern, as seen in the example above.

While at(route, x, y) works equally with both components and prop values (and actually with any other route-based values), the component-, config-, and file-based approaches that many routers tend to adopt are focused on component rendering, requiring an extra route matching hook for route-based prop values.

🔹 at() calls are independent from each other, they don't have to maintain a certain order, they shouldn't be necessarily grouped in a single component (although they can be, as in the example above). Components with route-based logic can be split like any other components.

🔹 Route-based rendering with the React's <Activity> component looks similar to what we've seen in the example above:

// Without Activity
{at("/about", <About/>)}

// With Activity
<Activity mode={at("/about", "visible", "hidden")}>
  <About/>
</Activity>

Navigation

The route navigation API is largely aligned with the similar native JS APIs familiar to most web developers, such as <a href="/x"> and window.location:

+ import { A, useRoute } from "@t8/react-router";

  let UserNav = ({ signedIn }) => {
+   let { route } = useRoute();

    let handleClick = () => {
-     window.location.assign(signedIn ? "/profile" : "/login");
+     route.assign(signedIn ? "/profile" : "/login");
    };

    return (
      <nav>
-       <a href="/">Home</a>
+       <A href="/">Home</A>
        <button onClick={handleClick}>Profile</button>
      </nav>
    );
  };

🔹 The route object has: .assign(url), .replace(url), .reload(), .href, .pathname, .search, .hash, .back(), .forward(), .go(delta) — similar to the built-in APIs of window.location and history carried over to route-based SPA navigation.

🔹 A route link component can be switched to the replace mode by having the data-navigation-mode="replace" attribute. In the replace mode, clicking the link will replace the current history navigation entry rather than keep it as a previous record (similarly to calling route.replace(url)), effectively preventing the user from returning to the current URL by pressing the browser's Back button.

🔹 Like the route link <A> corresponds to the HTML link tag <a>, the route link <Area> corresponds to the HTML link tag <area>.

Middleware

The useNavigationStart() and useNavigationComplete() hooks define routing middleware, that is intermediate actions to be done before and after the route navigation occurs:

import { useNavigationComplete, useNavigationStart } from "@t8/react-router";

function setTitle(href) {
  if (href === "/intro")
    document.title = "Intro";
}

let App = () => {
  let { route } = useRoute();
  let [hasUnsavedChanges, setUnsavedChanges] = useState(false);

  let handleNavigationStart = useCallback(nextHref => {
    if (hasUnsavedChanges)
      return false; // prevents navigation

    if (nextHref === "/intro") {
      route.assign("/"); // redirection
      return false;
    }
  }, [hasUnsavedChanges, route]);

  useNavigationStart(handleNavigationStart);
  useNavigationComplete(setTitle);

  return (
    // app content
  );
};

This example shows some common examples of what can be handled with routing middleware: preventing navigation with unsaved user input, redirecting to another location, setting the page title based on the current location.

🔹 The callback of both hooks is first called when the component gets mounted if the route is already in the navigation-complete state.

URL parameters

URL parameters, as a portion of the app's state, can be managed in the React's useState()-like manner, allowing for quick migration from local state to URL parameters or the other way around:

+ import { useRouteState } from "@t8/react-router";

  let App = () => {
-   let [{ coords }, setState] = useState({ coords: {} });
+   let [{ query }, setState] = useRouteState("/");

    let setPosition = () => {
      setState(state => ({
        ...state,
-       coords: {
+       query: {
          x: Math.random(),
          y: Math.random(),
        },
      });
    };

    return (
      <>
        <h1>Shape</h1>
-       <Shape x={coords.x} y={coords.y}/>
+       <Shape x={query.x} y={query.y}/>
        <p><button onClick={setPosition}>Move</button></p>
      </>
    );
  };

Route state live demo Typed route state live demo

Type safety

Type-safe routing is as an optional enhancement. It's enabled by supporting route patterns created with a type-safe URL builder like url-shape together with a schema created with a validation library implementing the Standard Schema spec, like zod, valibot, arktype, or yup. This approach allows for gradual or partial adoption of type-safe routing in an application.

import { A, useRoute } from "@t8/react-router";
import { createURLSchema } from "url-shape";
import { z } from "zod";

const { url } = createURLSchema({
  "/": z.object({}), // Goes without parameters
  "/sections/:id": z.object({
    // Path components
    params: z.object({
      id: z.coerce.number(),
    }),
    // Similarly a `query` schema can be added here
  }),
});

let App = () => {
  let { at } = useRoute();

  return (
    <>
      <header className={at(url("/"), "full", "compact")}>
        <h1>App</h1>
        <nav>
          <A href={url("/")}>Intro</A>{" | "}
          <A href={url("/sections/:id", { params: { id: 1 } })}>Start</A>
        </nav>
      </header>
      {at(url("/"), <Intro/>)}
      {at(url("/sections/:id"), ({ params }) => (
        <Section id={params.id}/>
      ))}
    </>
  );
};

Type-safe routing live demo

🔹 The url() function is a type-safe URL builder. It creates a URL with a URL pattern defined in the schema and typed parameters that are prevalidated against the given schema: typos and type mismatches are highlighted in a type-aware code editor. See url-shape for more details.

🔹 A URL schema doesn't have to cover the entire app. Standalone portions of an app can have their own URL schemas.

🔹 Optionally, application-wide type safety can be achieved by disallowing URLs and URL patterns other than provided by the URL builder (the url() function in the example above):

declare module "@t8/react-router" {
  interface Config {
    strict: true;
  }
}

Adding this type declaration to an app effectively disallows using string and RegExp values for routes and route patterns (such as in the route link href prop, route.assign(location), and the routing function at(routePattern, x, y)), only allowing values returned from the URL builder with the same routing APIs.

🔹 A URL builder pattern (like url("/sections/:id")) can also be used with useRouteState(pattern) and useRouteMatch(pattern) to manipulate URL parameters in a type-safe manner.

Typed URL parameters state demo

🔹 Recap: It's using typed URL patterns (like from url() of url-shape) that enables type-safe route handling, which is an optional enhancement. Plain string routes and RegExp route patterns are handled with baseline typing sufficient in many cases.

Nested routes

Nested routes don't require special rendering rules. All routes are handled equally and independently from each other.

let App = () => {
  let { at } = useRoute();

  return (
    <>
      {at("/about", <About/>)}
      {at("/about/contacts", <Contacts/>)}
      // ...
    </>
  );
};

In a type-safe setup, a URL schema of a nested route can inherit certain parameters from the parent route. Such relations (which might as well be other than direct nestedness) can be settled within the URL schema with the schema toolset.

import { createURLSchema } from "url-shape";
import { z } from "zod";

let sectionParams = z.object({
  sectionId: z.coerce.number(),
});

export const { url } = createURLSchema({
  "/sections/:sectionId": z.object({
    params: sectionParams,
  }),
  "/sections/:sectionId/stories/:storyId": z.object({
    params: z.object({
      ...sectionParams.shape, // Shared params
      storyId: z.string(),
    }),
  }),
});

Live typed nested routes demo

In such a setup, arbitrary relations between the routes are seen and managed directly, allowing for fine-grained control, including sharing or filtering out certain parameters.

Location provider

Server-side rendering and unit tests are the examples of the environments lacking a global location object (such as window.location). They are the prime use cases for the location provider, <Router>.

Let's consider an express application route as an example:

import { renderToString } from "react-dom/server";
import { Router } from "@t8/react-router";

app.get("/", (req, res) => {
  let html = renderToString(
    <Router location={req.originalUrl}>
      <App/>
    </Router>,
  );

  res.send(html);
});

The value passed to the router's location prop can be accessed via the useRoute() hook:

let { route, at } = useRoute();

console.log(route.href); // returns the router's `location`

Both route and at() returned from useRoute() operate based on the router's location.

<Router> can be used with client-side rendering as well. In most cases, it is unnecessary since by default the route context takes the global location from window.location if it's available.

Custom routing behavior

Custom routing behavior example

In this example, we've got a kind of a browser-in-browser with its routing based on a text input rather than the URL. It's enabled by passing an instance of a custom extension of the Route class, InputRoute, to the <Router> component, configured to interact with a text input.

This example also shows how the same routing code (of the <Content> component) can interact with either the URL or the text input element based on the closest <Router> component up the component tree.

Unknown routes

The fallback parameter of the route-matching function at(route, x, y) can be used as a way to handle unknown routes, as shown in the example below. In a type-safe setup, unknown routes can be handled based on whether the given route belongs to the URL schema (e.g. with validate(route) from url-shape).

import { A, useRoute } from "@t8/react-router";

const routeMap = {
  intro: "/intro",
  sections: /^\/sections\/(?<id>\d+)\/?$/,
};

const knownRoutes = Object.values(routeMap);

let App = () => {
  let { at } = useRoute();

  return (
    <>
      <nav>
        <A href={routeMap.intro}>Intro</A>
      </nav>
      {at(routeMap.intro, <Intro/>)}
      {at(routeMap.sections, ({ params }) => (
        <Section id={params.id}/>
      ))}
      {at(knownRoutes, null, <Error/>)}
    </>
  );
};

The last at() in this example results in null (that is no content) for all known routes and renders the error content for the rest unknown routes.

🔹 at() calls don't have to maintain a specific order, and the at() call handling unknown routes doesn't have to be the last.

🔹 at() calls don't have to be grouped side by side like in the example above, their collocation is not a requirement. at() calls are not coupled together, they can be split across separate components and files (like any other conditionally rendered components).

Lazy routes

Lazy routes are routes whose content is loaded on demand, when the route is visited.

Enabling lazy routes doesn't require a specific routing setup. It's a combination of the route matching and lazily loaded React components (with React.lazy() and React's <Suspense>), processed by a code-splitting-capable build tool (like Esbuild, Webpack, Rollup, Vite):

  import { useRoute } from "@t8/react-router";
+ import { Suspense } from "react";
- import { Projects } from "./Projects";
+ import { Projects } from "./Projects.lazy";

  let App = () => {
    let { at } = useRoute();

    return (
      <>
        // ...
        {at("/projects", (
-         <Projects/>
+         <Suspense fallback={<p>Loading...</p>}>
+           <Projects/>
+         </Suspense>
        ))}
      </>
    );
  };
+ // Projects.lazy.ts
+ import { lazy } from "react";
+
+ export const Projects = lazy(() => import("./Projects"));

Lazy routes live demo

In this example, the <Projects> component isn't loaded until the corresponding /projects route is visited. When it's first visited, while the component is being fetched, <p>Loading...</p> shows up, as specified with the fallback prop of <Suspense>.

Converting HTML links to SPA route links

A chunk of static HTML content is an example where the route link component can't be directly used but it still might be desirable to make plain HTML links in that content behave as SPA route links. The useRouteLinks() hook can be helpful here:

import { useRef } from "react";
import { useRouteLinks } from "@t8/react-router";

let Content = ({ value }) => {
  let containerRef = useRef(null);

  useRouteLinks(containerRef);

  return (
    <div ref={containerRef}>
      {value}
    </div>
  );
};

In this example, the useRouteLinks() hook makes all HTML links inside the container referenced by containerRef act as SPA route links.

A selector, or an HTML element, or a collection thereof, can be passed as the second parameter of useRouteLinks() to narrow down the relevant link elements:

useRouteLinks(containerRef, ".content a");