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

@uistate/router

v1.0.1

Published

SPA router for EventState stores — routing is just state

Downloads

180

Readme

@uistate/router

SPA router for EventState stores. Routing is just state.

navigate() writes to store paths. Components subscribe. No framework required.

Install

npm install @uistate/router

Quick Start

import { createEventState } from '@uistate/core';
import { createRouter } from '@uistate/router';

const store = createEventState({ state: {} });

const router = createRouter({
  routes: [
    { path: '/', view: 'home', component: HomeView },
    { path: '/users', view: 'users', component: UsersView },
    { path: '/users/:id', view: 'user', component: UserView },
    { path: '/users/:id/posts/:postId', view: 'post', component: PostView },
  ],
  store,
  fallback: { path: '/*', view: '404', component: NotFoundView },
  debug: true,
});

router.start();

How It Works

Every navigation writes to the store:

| Store Path | Value | |---|---| | ui.route.view | The matched view string (e.g. 'user') | | ui.route.path | The normalized path (e.g. '/users/42') | | ui.route.params | Extracted params (e.g. { id: '42' }) | | ui.route.query | Parsed query params (e.g. { tab: 'posts' }) | | ui.route.transitioning | true during navigation, false after |

Your components subscribe to these paths like any other state:

store.subscribe('ui.route.view', (view) => {
  console.log('View changed to:', view);
});

store.subscribe('ui.route.params', (params) => {
  console.log('Route params:', params);
});

// Wildcard: react to any route change
store.subscribe('ui.route.*', ({ path, value }) => {
  console.log('Route state changed:', path, value);
});

Route Patterns

Routes support static paths and dynamic :param segments:

{ path: '/',                view: 'home' }       // exact match
{ path: '/users',           view: 'users' }      // exact match
{ path: '/users/:id',       view: 'user' }       // dynamic segment
{ path: '/posts/:id/edit',  view: 'edit-post' }  // mixed

Params are extracted and available at ui.route.params:

// URL: /users/42
store.get('ui.route.params');  // { id: '42' }

View Components

A view component is any object with a boot method:

const UserView = {
  async boot({ store, el, signal, params }) {
    el.innerHTML = `<h1>User ${params.id}</h1>`;

    // Use signal for cleanup-aware async work
    const res = await fetch(`/api/users/${params.id}`, { signal });
    const user = await res.json();
    el.innerHTML = `<h1>${user.name}</h1>`;

    // Return an unboot function for cleanup
    return () => {
      console.log('UserView unmounted');
    };
  }
};

The boot function receives:

| Param | Description | |---|---| | store | The EventState store instance | | el | The root DOM element (from rootSelector) | | signal | An AbortSignal — aborted if the user navigates away before boot finishes | | params | Extracted route params (e.g. { id: '42' }) |

API

createRouter(config)

Returns a router instance.

Config options:

| Option | Type | Default | Description | |---|---|---|---| | routes | Array | [] | Route definitions | | store | Object | — | EventState store | | rootSelector | string | '[data-route-root]' | CSS selector for the mount point | | fallback | Object | null | Fallback route for unmatched paths | | debug | boolean | false | Log navigation to console | | linkSelector | string | 'a[data-link]' | Selector for intercepted link clicks |

Router Instance

router.start()

Starts listening for link clicks and popstate events. Immediately navigates to the current URL.

router.stop()

Removes event listeners and calls the current view's unboot function.

router.navigate(pathname, opts?)

Programmatic navigation.

router.navigate('/users/42');
router.navigate('/search', { search: '?q=hello' });
router.navigate('/users', { replace: true });

Options: { replace, search, restoreScroll }

router.navigateQuery(patch, opts?)

Patch query parameters without changing the path.

router.navigateQuery({ tab: 'posts' });           // add/update
router.navigateQuery({ tab: null });               // remove
router.navigateQuery({ page: '2', sort: 'name' }); // multiple

router.getCurrent()

Returns { view, path, search } for the current route.

Link Interception

Any <a> matching linkSelector (default: a[data-link]) is intercepted for client-side navigation:

<nav>
  <a href="/" data-link>Home</a>
  <a href="/users" data-link>Users</a>
  <a href="/users/42" data-link>User 42</a>
</nav>

<div data-route-root></div>

Standard browser behavior is preserved for:

  • External links (different origin)
  • Modified clicks (Ctrl, Cmd, Shift, Alt, right-click)
  • Links without data-link

Active Nav (Subscribe, Don't Bake In)

The router does not manage active nav styles. Instead, subscribe to the route path and manage your own UI:

store.subscribe('ui.route.path', (path) => {
  document.querySelectorAll('nav a[data-link]').forEach(a => {
    const href = new URL(a.getAttribute('href'), location.href).pathname;
    a.classList.toggle('active', href === path);
  });
});

This keeps the router focused on state. Your nav, your rules.

Base Path Support

If your app is served from a subdirectory, add a <base> tag:

<base href="/my-app/">

The router automatically detects it and adjusts all path operations.

Scroll Restoration

The router saves scroll positions per route and restores them on back/forward navigation. Forward navigation scrolls to top.

Accessibility

On every navigation, the router:

  1. Sets tabindex="-1" on the root element (if not already set)
  2. Focuses the root element (with preventScroll)

This ensures screen readers announce the new content.

CSS Hooks

The router sets attributes on <html> for CSS-driven transitions:

/* Style based on current view */
[data-view="home"] .hero { display: block; }
[data-view="user"] .sidebar { display: flex; }

/* Transition states */
[data-transitioning="on"] [data-route-root] {
  opacity: 0.5;
  pointer-events: none;
}

Testing

Two-layer testing architecture:

self-test.js — Zero-dependency self-test (35 assertions). Runs automatically on npm install via postinstall. Tests the pure-function core: pattern compilation, path normalization, route resolution, and URL-encoded param decoding.

node self-test.js

tests/router.test.js — Integration tests via @uistate/event-test (13 tests). Tests the store-driven routing patterns: setMany for atomic route updates, wildcard subscriptions, ui.route.go navigation, transition state, and type generation.

npm test

| Suite | Assertions | Dependencies | |-------|-----------|-------------| | self-test.js | 35 | none (zero-dep) | | tests/router.test.js | 13 | @uistate/event-test, @uistate/core |

Philosophy

Routing is not special. It's a set call to a path in a JSON tree. The router writes ui.route.*, and anything that cares about routing subscribes to ui.route.*. The router doesn't know about your nav, your breadcrumbs, your analytics, or your loading spinners. They all subscribe independently. That's UIState: EventState + Routing.

License

MIT