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

@slimr/router

v3.0.7

Published

A tiny, reactive router for React. ~3kb gzipped. Zero config links. Stack routing. Scroll restore. Built on `@slimr/observable` for first-class reactive state.

Readme

🪶 @slimr/router npm package

A tiny, reactive router for React. ~3kb gzipped. Zero config links. Stack routing. Scroll restore. Built on @slimr/observable for first-class reactive state.

Why this over react-router? You get stack routing (like react-navigation, but for the web), automatic same-site link interception (no <Link> component needed), scroll restoration, and reactive query params — all in a fraction of the bundle.

npm install @slimr/router

Quick start

// router.ts — define your routes
import { Router } from "@slimr/router"

export const router = new Router({
  home:     { path: "/",              exact: true,  component: Home },
  user:     { path: "/user/:id",                   component: User },
  notFound: { path: "/",              exact: false, component: NotFound },
})
// app.tsx — render the matched route
import { Switch } from "@slimr/router"
import { router } from "./router"

export function App() {
  return <Switch router={router} />
}
// pages/user.tsx — subscribe to route changes in any component
import { router } from "../router"

export default function User() {
  const route = router.route$.use()        // re-renders when route changes
  return <h1>User {route.urlParams!.id}</h1>
}

That's it. No providers. No context wrappers. No nesting.

Features

Reactive state via observables

Route and query param state are exposed as @slimr/observable instances:

const route = router.route$.use()                          // React hook — re-renders on route change
const searchParams = router.searchParams$.use()            // React hook — re-renders on any URL change
const unsubscribe = router.route$.subscribe(cb)            // imperative subscription
const currentMatch = router.route$.val                     // read without subscribing

For granular re-renders, use useObservable with a selector:

import { useObservable } from "@slimr/router"

// Only re-render when urlParams changes
const params = useObservable(router.route$, { select: m => m.urlParams })

// Only re-render when a specific query param changes
const editId = useObservable(router.searchParams$, { select: sp => sp.get("edit") })

Stack routing

Routes can be grouped into a stack — a nested history within the page, like native mobile navigation. Navigate forward to push, hit back to pop, or use special hashes to control the stack:

const router = new Router({
  photos:        { path: "/photos",         isStack: true, component: Photos },
  "photos.item": { path: "/photos/:id",                  component: PhotoDetail },
  notFound:      { path: "/",               exact: false,  component: NotFound },
})
<a href="/photos/1">View photo 1</a>       {/* pushes onto stack */}
<a href="/photos/2">View photo 2</a>       {/* pushes onto stack */}
<a href="#back">Back</a>                   {/* pops from stack */}
<a href="#clear">Exit stack</a>            {/* clears stack history */}

Stack routing is useful for detail views, multi-step flows, and any UI where you want a "drill-down" navigation pattern without losing the parent page's state.

Automatic link handling

Every <a> tag pointing to the same origin is automatically intercepted — no <Link> component required:

// These just work:
<a href="/about">About</a>
<a href={router.routes.user.toPath({ id: "42" })}>User 42</a>
<a href="/form#replace">Replace current history entry</a>

External links and target="_blank" links are left alone. Browser back/forward and popstate are handled too.

Scroll restoration

After a route change, the router restores the scroll position when navigating back. Works with a custom scroll container:

const router = new Router(routes, {
  scrollElSelector: "main"   // document.querySelector("main") instead of window
})

Type-safe route linking

Routes are defined as a plain object, so you get autocomplete and compile-time checking on route keys and params:

router.routes.user.toPath({ id: "42" })         // "/user/42"
router.routes.user.toPath({ id: "42", tab: "settings" })  // "/user/42?tab=settings"
router.goto(router.routes.user, { id: "42" })    // navigates to /user/42

No string-based route names. No broken links at runtime.

Navigating programmatically

router.goto("/about")                              // push a new entry
router.goto(router.routes.user, { id: "42" })     // push with typed params
router.replace("/home")                            // replace current entry
await router.goto("/about")                        // await subscriber notification

API

new Router(routes, options?)

Create a router instance.

| Option | Type | Default | Description | |--------|------|---------|-------------| | scrollElSelector | string | undefined | CSS selector for the scroll container. If set, scroll restoration targets this element instead of window. |

router.route$

An ObservableR<RouteMatch> that fires when the matched route changes.

| Method | Returns | Description | |--------|---------|-------------| | .use() | RouteMatch | React hook — re-renders component on route change | | .subscribe(cb) | () => void | Imperative subscription — returns an unsubscribe function | | .val | RouteMatch | Current value — read without subscribing |

router.searchParams$

An ObservableR<URLSearchParams> that fires on every URL change, including same-route query param changes.

| Method | Returns | Description | |--------|---------|-------------| | .use() | URLSearchParams | React hook — re-renders on any URL change | | .subscribe(cb) | () => void | Imperative subscription | | .val | URLSearchParams | Current search params |

router.routes

A map of route keys to enhanced route objects.

router.routes.user.key           // "user"
router.routes.user.path          // "/user/:id"
router.routes.user.toPath({ id: "42" })  // "/user/42"
router.routes.user.isMatch("/user/42")   // { id: "42" } or false

router.current

A live, non-reactive snapshot of the current URL state. Recomputes on every access.

router.current.route          // current RouteMatch
router.current.url            // full URL string
router.current.path           // pathname + search
router.current.search         // search string ("?foo=bar")
router.current.searchParams   // URLSearchParams instance
router.current.scrollTop      // current scroll position

router.goto(route, urlParams?)

Navigate by pushing a new history entry. Accepts a route object, route key string, or raw path string. Returns Promise<void>.

router.replace(route, urlParams?)

Same as goto but replaces the current history entry instead of pushing.

router.onLoad()

Called by <Switch> after rendering a new route to restore scroll position. You usually don't call this directly.

<Switch router={router} />

Renders the component for the first matching route. Handles scroll restoration automatically.

useObservable(router.route$, { select })

Re-exported from @slimr/observable/react for convenience. Subscribe to a slice of the observable value — the component only re-renders when the selected slice changes (deep equality).

Migrating from v2

| v2 | v3 | |----|-----| | router.use() | router.route$.use() | | router.subscribe(fn) | router.route$.subscribe(fn) | | router.unsubscribe(fn) | const unsub = router.route$.subscribe(fn); unsub() | | <Component route={route} url={url} /> from Switch | const route = router.route$.use() inside the component |

Comparisons

react-router

Pros: More mature. SSR support. Larger ecosystem.

Cons: Bundle size. No stack routing. Requires <Link> components, <Routes> wrappers, and nested <Outlet> patterns.

Next.js router

Pros: File-system routing. Built-in SSR/SSG. Zero config for routing.

Cons: Requires Next.js. No stack routing. Inflexible for custom routing patterns.

react-navigation (React Native)

Pros: Very flexible and feature-rich for native navigation.

Cons: Not designed for the web. Large bundle. Steep learning curve.