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

sidestate

v0.1.16

Published

Vanilla TS/JS shared state management and routing

Downloads

1,697

Readme

sidestate

Vanilla TS/JS state management for sharing data across decoupled parts of the code and routing. Routing is essentially shared state management, too, with the shared data being the URL.

This package exposes the following classes:

EventEmitter ──► State ──► PersistentState
                    │
                    └────► URLState ──► Route

Roughly, their purpose boils down to the following:

  • EventEmitter is for triggering actions without tightly coupling the interacting components
  • State is EventEmitter that stores data and emits an event when the data gets updated, it's for dynamic data sharing without tight coupling
  • PersistentState is State that syncs its data to the browser storage and restores it on page reload
  • URLState is State that stores the URL + syncs with the browser's URL in a SPA fashion
  • Route is URLState + native-like APIs for SPA navigation and an API for URL matching

Contents: State · PersistentState · Route · Annotated examples · Integrations

State

A thin data container for dynamic data sharing without tight coupling.

import { State } from "sidestate";

const counterState = new State(42);

document.querySelector("button").addEventListener("click", () => {
  counterState.setValue((value) => value + 1);
});

counterState.on("set", ({ current }) => {
  document.querySelector("output").textContent = String(current);
});

In this example, a button changes a counter value and an <output> element shows the updating value. Both elements are only aware of the shared counter state, but not of each other.

A "set" event callback is called each time the state value changes and immediately when the callback is added. Subscribe to the "update" event to have the callback respond only to the subsequent state changes without the immediate invocation.

PersistentState

A variety of State that syncs its data to the browser storage and restores it on page reload. Otherwise, almost identical to State in usage.

- import { State } from "sidestate";
+ import { PersistentState } from "sidestate";

- const counterState = new State(42);
+ const counterState = new PersistentState(42, { key: "counter" });

  document.querySelector("button").addEventListener("click", () => {
    counterState.setValue((value) => value + 1);
  });

  counterState.on("set", ({ current }) => {
    document.querySelector("output").textContent = String(current);
  });

By default, PersistentState stores its data at the specified key in localStorage and transforms the data with JSON.stringify() and JSON.parse(). Switch to sessionStorage by setting options.session to true in new PersistentState(value, options). Set custom serialize() and deserialize() in options to override the default data transforms used with the browser storage. Alternatively, use custom { read(), write()? } as options to set up custom interaction with an external storage.

Instances of PersistentState automatically sync their values with the browser storage when created and updated. At other times, call .emit("sync") on a PersistentState instance to sync its value from the browser storage when needed.

Route

Stores the URL, exposes a native-like API for SPA navigation and an API for URL matching.

import { Route } from "sidestate";

const route = new Route();

Navigate to other URLs in a SPA fashion similarly to the native APIs:

route.href = "/intro";
route.assign("/intro");
route.replace("/intro");

Or in a more fine-grained manner:

route.navigate({ href: "/intro", history: "replace", scroll: "off" });

Check the current URL value like a regular string with route.href:

route.href === "/intro";
route.href.startsWith("/sections/");
/^\/sections\/\d+\/?/.test(route.href);

Or, alternatively, with route.at(url, x, y) which is similar to the ternary conditional operator atURL ? x : y:

document.querySelector("header").className = route.at("/", "full", "compact");

Use route.at(url, x, y) with dynamic values that require values from the URL pattern's capturing groups:

document.querySelector("h1").textContent = route.at(
  /^\/sections\/(?<id>\d+)\/?/,
  ({ params }) => `Section ${params.id}`,
);

Enable SPA navigation with HTML links inside the specified container (or the entire document) without any changes to the HTML:

route.observe(document);

Tweak the links' navigation behavior by adding a relevant combination of the optional data- attributes (corresponding to the route.navigate() options):

<a href="/intro">Intro</a>
<a href="/intro" data-history="replace">Intro</a>
<a href="/intro" data-scroll="off">Intro</a>
<a href="/intro" data-spa="off">Intro</a>

Define what should be done when the URL changes:

route.on("navigationcomplete", ({ href }) => {
  renderContent();
});

Define what should be done before the URL changes (in a way effectively similar to routing middleware):

route.on("navigationstart", ({ href }) => {
  if (hasUnsavedInput)
    return false; // Quit the navigation, prevent the current URL change

  if (href === "/") {
    route.href = "/intro"; // SPA redirection
    return false;
  }
});

Annotated examples

Find also the code of these examples in the repo's tests directory.

Integrations

react-sidestate