Skip to content

React

@isorouter/react wraps the core router's immutable-snapshot external store with useSyncExternalStore, so navigation re-renders stay correct and tear-free under React 18's concurrent renderer.

Requires React ≥ 18. @isorouter/core is installed automatically.

sh
npm install @isorouter/react

Quick start

tsx
import { createRoot } from "react-dom/client";
import { createRouter, Router, Outlet } from "@isorouter/react";

const router = createRouter([
  { path: "/", component: Home },
  { path: "about", component: About },
  {
    path: "dashboard",
    component: DashboardLayout,
    children: [
      { index: true, component: Overview },
      { path: "settings", component: Settings },
    ],
  },
] as const);

function DashboardLayout() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Outlet />
    </div>
  );
}

createRoot(document.getElementById("root")!).render(
  <Router router={router} notFound={<p>Not found</p>} />,
);

createRouter is createCoreRouter with the component type fixed to a React ComponentType. <Router> calls router.start() on mount and router.stop() on unmount.

Components

<Router>

tsx
<Router
  router={router}
  loading={<Spinner />}
  notFound={<NotFound />}
  error={(err) => <ErrorPage error={err} />}
/>
  • notFound — rendered when snapshot.status === "not-found".
  • error — called with snapshot.error when snapshot.status === "error".
  • loading — when there's no matched root component yet (e.g. before the first commit) and neither error nor notFound applies.

Otherwise renders the root matched component, snapshot.components[0].

<Outlet>

Renders the next component in the matched chain at the current nesting depth; renders nothing when there's no matching child. Use it inside a layout.

tsx
<Link href="/dashboard" activeClassName="active" exact>
  Dashboard
</Link>

A plain <a> intercepted by the Navigation API — extends AnchorHTMLAttributes<HTMLAnchorElement> and forwards ref and any other props. activeClassName is appended to className when router.isActive(href, { exact }) (default "active"); when active, also sets aria-current="page". See Links & active state.

Hooks

All hooks must be used within <Router>.

HookReturns
useRouter()the Router instance.
useRouterState()the current RouterSnapshot, re-rendering on every commit.
useParams<P>()snapshot.params, typed as P.
useLocation()snapshot.url.
useNavigate()a referentially-stable (to, opts?) => void bound to router.navigate.
tsx
import { useParams, useNavigate } from "@isorouter/react";

function User() {
  const { id } = useParams<{ id: string }>();
  const navigate = useNavigate();
  return <button onClick={() => navigate("/")}>User {id} — go home</button>;
}

useNavigate() is referentially stable, so it's safe in useEffect dependency arrays and useCallback bodies without re-subscribing.

See also

Released under the MIT License.