@k8ordo/router

Find where you are

To mark the link to the page you are on, or to show that the next page is loading, you ask where the browser is. This page covers the hooks and functions that answer.

On this page

Read the current path

usePathname returns the path the browser is on, with any trailing slash dropped.

src/current-path.tsx
import { usePathname } from '@k8ordo/router';

export function CurrentPath() {
  const pathname = usePathname();
  return <p>You are at {pathname}</p>;
}

It re-renders when the path changes and never when the query string does. The query string is left out because reading it is @k8ordo/state’s job, where a component re-renders only when a value it reads changes.

The path is spelled as the URL spells it, not decoded: characters such as Japanese come back percent-encoded.

usePathname reads the browser’s URL rather than the route table, so it works the same under @k8ordo/static and @k8ordo/server.

Check whether a page is open

useMatch returns the params when the current path fits a pattern, and null when it does not. It is what marks a link in a navigation.

src/products-link.tsx
import { href, useMatch } from '@k8ordo/router';

export function ProductsLink() {
  const isCurrent = useMatch('/products') !== null;

  return (
    <a
      aria-current={isCurrent ? 'page' : undefined}
      href={href('/products')}
    >
      Products
    </a>
  );
}

Whether a link is current is something you ask, not a prop you pass it. With no <Link> component in between, how the link is marked is up to you.

Check whether you are inside a section

A pattern followed by /* means “anywhere below it”. Use it to know which section is open, as a sidebar does.

ts
matchPath('/products/*', '/products/42'); // {}
matchPath('/products/*', '/products'); // null
matchPath('/products/*', '/products', { inclusive: true }); // {}

matchPath makes the same check as useMatch against a path you have in hand; the example uses it to show the cases side by side.

/products/* does not match /products itself. To mark the section’s own entry page as well, pass { inclusive: true }.

/* can only follow a pattern in the route table. With Register augmented, a pattern the table lacks is a type error.

useMatch is built on usePathname, so it re-renders only when the path changes, and it needs no route table, so it works under the framework too. This site’s header makes the same check with inclusive, marking a package on its landing page and on every page below it.

Playground

Try matching a pattern

This site runs on @k8ordo/router too. The demo starts from the path usePathname reads on this page, and matchPath answers again as you edit.

usePathname()/en/router/location

Call
matchPath('/:locale/router/*', '/en/router/location')
Result
{"locale":"en"}

Try it

  1. It starts with the pattern /:locale/router/* and this page’s path. The result is {"locale":"en"}: what /* took is not a param.
  2. Change the path to /en/router. The result is null, because /* does not match the pattern’s own page.
  3. Turn on “inclusive”. The result is {"locale":"en"} again.
  4. Add a / to the end of the path. The result does not change, because the trailing slash is dropped before comparing.

Know which page is loading

On a page change the URL changes first, and the new page appears once it is ready. On a slow navigation, then, usePathname already returns the new path while the previous page is still on screen.

usePendingPathname returns the path of the page being loaded, or null when nothing is.

src/progress.tsx
import { usePendingPathname } from '@k8ordo/router';

export function Progress() {
  const pending = usePendingPathname();
  if (pending === null) return null;

  return <p role="status">Loading {pending}…</p>;
}

It is set as the navigation starts, and goes back to null once the new page is on screen. It also goes back to null when the navigation is abandoned or its load fails.

An update that changes only the query string is not a page change and sets nothing. A framework page that reads the query string and loads again for it is the exception: that is a load in progress like any other.

Read a page’s params

A page rendered by <Router> reads its params by handing useParams its own pattern. The type comes from the pattern string, and every value is a string.

src/pages/product-page.tsx
import { useParams } from '@k8ordo/router';

export function ProductPage() {
  const { id } = useParams('/products/:id');
  return <h1>Product {id}</h1>;
}

The pattern also says “this component renders as the page at this pattern”. Rendered under another pattern, it throws this instead of returning params of the wrong shape:

text
useParams("/products/:id") rendered under "/products"

A component shared by several pages can use useRoute instead, which returns the pattern that won and its params, untyped.

ts
const { pattern, params } = useRoute();
Warning

Pitfall

useParams and useRoute read the match <Router> holds. Under @k8ordo/static and @k8ordo/server the browser has no route table, so neither works there; a page receives params as a prop.

k8ordo

React libraries that use Baseline features without holding back.

© 2026 k8o — MIT License

Typeset in Noto Sans JP & M PLUS 2