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 🙏

© 2026 – Pkg Stats / Ryan Hefner

nanoroute

v0.2.0

Published

Zero-dependency React router in ~2KB gzipped — nested routes, typed params, wildcards, and a memory history, no context provider required.

Readme

nanoroute

A tiny, dependency-free router for React 19+. Nested routes, typed params, wildcards, search params, and a memory history for tests — client-side routing with nothing extra bolted on.

npm version bundle size zero dependencies types included license

Contents

Why nanoroute

  • ~2.3KB gzipped, zero runtime dependencies. No transitive packages, no supply-chain surface beyond React itself.
  • No provider needed. Location lives in a useSyncExternalStore store, not in context — mount <Routes> anywhere and it just works against the browser URL.
  • No leaks by construction. One shared popstate/hashchange listener exists only while components are mounted, and every cache in the library is bounded.
  • React Compiler friendly. Passes eslint-plugin-react-hooks v7 with the compiler rules enabled; every manual memo is preserved rather than skipped.
  • ESM, fully typed, tree-shakeable. Ships its own .d.ts, no any in the public API.
  • A real memory history, not a mock. MemoryRouter and createMemoryHistory make route-aware components testable without touching window.

nanoroute vs. react-router

react-router is a capable, full-featured framework: data loaders, actions, fetchers, deferred data, framework mode. If you need those, use it. If you just need to show the right component for the current URL, nanoroute does that and stops there.

| | nanoroute | react-router | | --- | --- | --- | | Gzipped size | ~2.3 KB | ~57.9 KB (main bundle) | | Runtime dependencies | 0 | 1 (cookie-es) | | Setup | Mount <Routes> — no provider required | Requires <BrowserRouter> / RouterProvider | | Scope | Routing primitives: match, navigate, params | Full framework: loaders, actions, data APIs, framework mode | | Config surface | path, element, nested children | Loaders, actions, error boundaries, handles, and more | | TypeScript | Fully typed, no any | Fully typed |

Sizes measured August 2026: nanoroute's dist/index.js gzips to 2,398 bytes (built from this repo); [email protected]'s main bundle gzips to 59,247 bytes per Bundlephobia. Re-check before citing — both numbers move as each package ships new releases.

Reach for nanoroute when you want SPA routing without adopting a framework's opinions. Reach for react-router when you want the data layer that comes with it.

Install

npm install nanoroute
pnpm add nanoroute
yarn add nanoroute

Requires React ≥ 19 (contexts are rendered directly as providers, and ref is a plain prop). No other peer dependencies.

Quick start

import { Link, Outlet, Route, Routes, useParams } from 'nanoroute'

const Shell = () => (
	<>
		<nav>
			<Link to="/">Home</Link>
			<Link to="/users/7">User</Link>
		</nav>
		<Outlet />
	</>
)

const User = () => <h1>{useParams<{ id: string }>().id}</h1>

export const App = () => (
	<Routes>
		<Route path="/" element={<Shell />}>
			<Route path="" element={<Home />} />              {/* index route */}
			<Route path="users/new" element={<NewUser />} />  {/* wins over :id */}
			<Route path="users/:id" element={<User />} />
			<Route path="files/*" element={<Files />} />      {/* params['*'] */}
			<Route path="*" element={<NotFound />} />
		</Route>
	</Routes>
)

No <BrowserRouter> wrapper — <Routes> reads the browser URL by default. Mount it anywhere in the tree.

Full export surface, for a quick scan:

import {
	// components
	Link, MemoryRouter, Navigate, Outlet, Route, Router, Routes,
	// hooks
	useLocation, useMatch, useNavigate, useParams, useRouterHistory, useSearchParams,
	// history & SSR
	browserHistory, createBrowserHistory, createMemoryHistory, createStaticHistory,
	navigate, setServerUrl,
	// standalone matcher
	matchPath,
} from 'nanoroute'

Guide

Routing & matching

Patterns are compiled once to a regex and ranked by specificity, so declaration order never matters: static segments beat :params, which beat *.

<Route path="users/new" element={<NewUser />} /> {/* static: always wins over :id */}
<Route path="users/:id" element={<User />} />    {/* dynamic: params.id */}
<Route path="files/*" element={<Files />} />     {/* wildcard: params['*'] */}

A trailing * also matches the bare parent path (files/* matches /files, with params['*'] equal to ''). Use matchPath(pattern, pathname) to run the same matcher outside of rendering, e.g. for route-based analytics or redirects.

Nested layouts

<Route> is configuration only — it never renders. <Routes> reads the tree of <Route> elements, matches the deepest one, and folds every ancestor's element around it so each layout's <Outlet /> renders its matched child.

<Routes>
	<Route path="/" element={<Shell />}>
		<Route path="dashboard" element={<DashboardLayout />}>
			<Route path="" element={<DashboardHome />} />
			<Route path="settings" element={<DashboardSettings />} />
		</Route>
	</Route>
</Routes>

<Outlet /> renders null where there's nothing nested — it's safe to include in every layout unconditionally.

Navigation

import { Link, Navigate, useNavigate } from 'nanoroute'

<Link to="/users/7">User 7</Link>
<Link to="/users/7" replace state={{ from: 'list' }}>User 7</Link>

<Link> accepts every <a> prop (including ref) plus to, replace, and state. Modified clicks (Cmd/Ctrl/Shift/Alt), a non-_self target, and cross-origin URLs all fall through to normal browser navigation instead of being intercepted.

const navigate = useNavigate()
navigate('/users/7')
navigate('/users/7', { replace: true, state: { from: 'list' } })
navigate(-1) // back one entry, like history.go(-1)

<Navigate to="/login" /> redirects on mount (replace defaults to true, unlike <Link>). For navigation outside of React — an error handler, a non-component module — use the module-level navigate(to, options), which targets the browser history directly.

Location, params & active links

import { useLocation, useMatch, useParams } from 'nanoroute'

const { pathname, search, hash, state } = useLocation()
const { id } = useParams<{ id: string }>()
const isActive = useMatch('/users/:id') !== null // handy for active-link styling

Search params

import { useSearchParams } from 'nanoroute'

const [searchParams, setSearchParams] = useSearchParams()
const tab = searchParams.get('tab')

setSearchParams({ tab: 'settings' })                 // replaces the query string
setSearchParams((current) => {
	current.set('page', '2')
	return current
})
setSearchParams({ tab: 'settings' }, { replace: false }) // push instead of replace

The setter replaces the current history entry by default, so filter and pagination controls don't flood back/forward history.

History & testing

Swap the browser out for an in-memory history — for tests, embedded panes, previews, or anywhere without a real address bar:

import { MemoryRouter } from 'nanoroute'

render(
	<MemoryRouter initialEntries={['/users/9']}>
		<App />
	</MemoryRouter>,
)

Bring your own instance to drive and inspect it directly, e.g. from node:test or Vitest:

import assert from 'node:assert/strict'
import { createMemoryHistory, Router } from 'nanoroute'

const history = createMemoryHistory({ initialEntries: ['/'] })
render(<Router history={history}><App /></Router>)

history.navigate('/users/7')
assert.deepEqual(history.entries, ['/', '/users/7'])
assert.equal(history.index, 1)

createMemoryHistory resolves relative targets ('sub', '?q=1', '#top', '../x') against the current entry, truncates forward entries on push (like a real session), and caps the stack at maxEntries (default 50) so a long-lived router can't grow it forever.

Inside a component, useRouterHistory() returns whichever history is active for that subtree — the browser by default, or whatever <Router> / <MemoryRouter> supplied — which is what useNavigate and useLocation are built on:

import { useRouterHistory } from 'nanoroute'
import type { MemoryHistory } from 'nanoroute'

const history = useRouterHistory()
history.navigate('/next')                    // always available, whatever the history
const { entries } = history as MemoryHistory // only valid when you know it's a MemoryRouter

Server-side rendering

import { setServerUrl } from 'nanoroute'

setServerUrl(request.url) // once per request, before renderToString / renderToStaticMarkup

On the client, the same snapshot is read from window.location, so hydration matches without any extra setup on your part.

setServerUrl writes one shared value for the whole process, so it's only safe when requests can't interleave — true for synchronous rendering, not for streaming SSR (renderToPipeableStream / renderToReadableStream), where a Suspense boundary can yield mid-render and let a second request's setServerUrl call stomp on the first. For that case, isolate the URL per request instead:

import { createStaticHistory, Router } from 'nanoroute'

const history = createStaticHistory(request.url) // scoped to this request, not shared
render(<Router history={history}><App /></Router>)

API reference

| Export | Notes | | --- | --- | | <Routes> / <Route> | path, element, nested children. <Route> is config only. | | <Outlet /> | Renders the matched child route. | | <Link to> | Plus replace, state, and every <a> prop including ref. Modified clicks, target, and cross-origin URLs fall through to the browser. | | <Navigate to> | Redirects on mount; replace defaults to true. | | <Router history> | Runs a subtree against any history. | | <MemoryRouter> | initialEntries, initialIndex, maxEntries. | | useNavigate() | (to, { replace, state }). to may be a delta: navigate(-1). | | useLocation() | { pathname, search, hash, state }. | | useParams<T>() | Params of the matched route. | | useSearchParams() | [URLSearchParams, setSearchParams]; the setter replaces by default. | | useMatch(pattern) | Params or null — handy for active links. | | useRouterHistory() | The history driving this subtree. | | navigate(to, opts) | Module-level, for use outside React. Targets the browser history. | | matchPath(pattern, pathname) | Standalone matcher. | | browserHistory | The singleton driving the browser by default — used when no <Router> wraps the tree. | | createBrowserHistory() | Creates an independent browser-backed history; rarely needed, browserHistory already exists. | | createMemoryHistory(options) | initialEntries, initialIndex, maxEntries. Returns a history with entries / index for inspection. | | createStaticHistory(url) | A history frozen to one URL, isolated per call — safe for concurrent SSR requests, unlike setServerUrl's shared global. | | setServerUrl(url) | Sets the URL browserHistory reports during synchronous SSR (renderToString / renderToStaticMarkup). |

All prop and return types (LinkProps, RouterHistory, MemoryHistory, RouteParams, SearchParamsUpdate, …) are exported from nanoroute directly — no @types package, no duplicated ambient types.

TypeScript

useParams and useMatch are the two spots you'll usually reach for a generic:

const { id } = useParams<{ id: string }>()

const params = useMatch('/files/*') // RouteParams | null
if (params) console.log(params['*']) // the wildcard capture

FAQ

What is nanoroute?

nanoroute is a tiny, dependency-free client-side router for React 19+. It matches the current URL against a tree of <Route> elements and renders the matched component, with support for nested layouts, dynamic params, wildcards, search params, and a memory history for tests.

Is nanoroute a good react-router alternative?

Yes, if you want URL-based rendering, nested layouts, and typed params without adopting a data-loading framework. It is not a drop-in replacement — nanoroute has no loaders, actions, or framework mode by design. See nanoroute vs. react-router.

Does nanoroute require a <BrowserRouter> provider?

No. <Routes> reads the browser URL by default with zero setup. Wrap in <Router> or <MemoryRouter> only when you want to swap in a different history, e.g. in tests.

Does nanoroute support data loaders, actions, or SSR frameworks like Remix or Next.js?

No — that's explicitly out of scope; see Deliberately not included. nanoroute does support server-side rendering itself via setServerUrl (synchronous rendering) or createStaticHistory (concurrent/streaming SSR), and works fine as the client-side router inside a custom SSR setup.

How do I test components that use nanoroute?

Render them inside <MemoryRouter initialEntries={[...]}>, or build a createMemoryHistory() instance and pass it to <Router history={...}> to drive and assert on navigation directly. See History & testing.

What React version does nanoroute require?

React 19 or newer. nanoroute renders context providers directly (<Context value={…}>) and passes ref as a plain prop, both React 19 features.

Is nanoroute CommonJS-compatible?

No, ESM only. CommonJS consumers need a bundler or await import('nanoroute').

Deliberately not included

Data loaders, lazy routes, scroll restoration, basename, hash history, relative <Routes> nesting, and NavLink (use useMatch for active styling). Reach for react-router if you need them.

Notes

  • ESM only. Consumers on CommonJS need a bundler or await import().
  • useLocation().state refreshes when the URL changes; a replaceState that only swaps state will not re-render on its own.

Contributing

npm run lint       # eslint src
npm run typecheck  # tsc -p tsconfig.json
npm run test       # compiles with tsc, then runs node --test
npm run build      # tsup -> dist/

Issues and pull requests: github.com/SheikhAminul/nanoroute.

License

MIT © Sheikh Aminul Islam