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

totally-tubular

v0.4.1

Published

A completely framework-agnostic, JavaScript / TypeScript state management and update broadcasting library wit zero dependencies, solid performance and a micro file size.

Readme

totally-tubular

A framework-agnostic JavaScript / TypeScript state management library with zero dependencies, a tiny file size, and solid performance.

Why another state management library?

State management has a tendency to get complicated fast. This library is an experiment in keeping things simple:

  • Portable. Your store is plain TypeScript. If you swap React for Svelte, or add a Node.js background worker, the store itself doesn't change.
  • Small. No framework coupling, no middleware pipeline, no magic — just a class that holds state, lets you read it, update it, and watch it for changes.
  • Honest. It mutates your state in place for performance. If you want immutability, return new objects from your updater functions.

Install

npm i totally-tubular --save

Core concepts

A Tubular instance wraps an object and gives you five things:

| Method | What it does | |---|---| | read(path) | Returns the current value at a dot-separated path | | update(path, fn) | Calls fn with the current value, stores the return value, then notifies all observers | | observe(path, fn) | Registers a callback that fires whenever path is updated | | unobserve(path, fn) | Removes a previously registered callback | | reset() | Restores every key to its initial value and notifies all observers of each change |

Paths are fully type-checked. TypeScript knows every valid dot-path into your state shape, so you get autocomplete and a compile error if you mistype a key.

Quick start

import { Tubular } from "totally-tubular";

interface AppState {
  isActive: boolean;
  count: number;
  meta: {
    title: string;
  };
  name: string;
}

const store = new Tubular<AppState>({
  isActive: false,
  count: 123,
  meta: { title: "Director" },
  name: "Test User",
});

// Watch a value for changes
store.observe("meta.title", (newVal, oldVal) =>
  console.info("title changed from", oldVal, "to", newVal),
);

console.info(store.read("meta.title")); // "Director"

// Update a value — all observers are notified immediately after
store.update("meta.title", (prev) => `Supreme ${prev}`);

console.info(store.read("meta.title")); // "Supreme Director"

Resetting state

Call reset() to restore your entire state back to the value you passed to the constructor. Every updated key is restored and every registered observer fires with the new (initial) value.

const store = new Tubular({
  name: "Alice",
  count: 0,
  settings: { theme: "light" },
});

store.update("name", () => "Bob");
store.update("count", (prev) => prev + 1);
store.update("settings.theme", () => "dark");

store.observe("name", (newVal, oldVal) => {
  console.log(`name: ${oldVal} -> ${newVal}`);
});

store.reset();
// Logs: name: Bob -> Alice
// Reads back: "Alice", 0, { theme: "light" }

React

Import useTubular from totally-tubular/react. It works exactly like React's built-in useState — you get back a [value, setter] tuple — except the value comes from your store and every component watching the same path re-renders when it changes.

import { Tubular } from "totally-tubular";
import { useTubular } from "totally-tubular/react";

interface AppState {
  user: { name: string };
  count: number;
}

// Create the store once, outside your components
const store = new Tubular<AppState>({
  user: { name: "Alice" },
  count: 0,
});

function Counter() {
  const [count, setCount] = useTubular(store, "count");

  return (
    <button onClick={() => setCount((n) => (n ?? 0) + 1)}>
      Clicked {count} times
    </button>
  );
}

function Greeting() {
  // Read-only — just destructure the first element
  const [name] = useTubular(store, "user.name");
  return <p>Hello, {name}!</p>;
}

The setter accepts either a plain value or an updater function:

// Plain value
setCount(10);

// Updater function (receives the current value)
setCount((prev) => (prev ?? 0) + 1);

useTubular automatically unsubscribes when the component unmounts, so you don't need to manage cleanup yourself.

Resetting from React

Import useTubularReset from totally-tubular/react. It returns a stable callback that restores the store to its initial state. Every useTubular hook watching the store re-renders with the restored value.

import { useTubularReset } from "totally-tubular/react";

function ResetButton() {
  const handleReset = useTubularReset(store);
  return <button onClick={handleReset}>Reset</button>;
}

The callback identity never changes, so it's safe to pass directly as an event handler or as a dependency to useEffect.

Things you should know

  • Arrays and nested objects are observed at their path, not their contents. observe("items", fn) fires when items itself is replaced. To add an item to an array, use update("items", prev => [...prev, newItem]).
  • Arbitrary array indices are not observable. You cannot do observe("items.0", fn). If you need per-item reactivity, store each item in its own Tubular instance.
  • State is mutated in place. totally-tubular does not clone your state on every update. This keeps things fast, but means the object you pass to new Tubular(initialState) will be modified directly. If that matters to you, pass a deep clone as the initial state.

Performance benchmarks

All benchmarks were run with Node.js 26.5.0 using the tsx TypeScript interpreter.

Apple M3 Max · 36 GB RAM · macOS Sequoia 15.5

-----shallow state object-----

1,000 updates to "name" string val with 10 observers: 0.0005343750000000114s
1,000 updates to "name" string val with 100 observers: 0.0006880840000000035s
1,000 updates to "name" string val with 1000 observers: 0.002814292000000023s
10,000 updates to "name" string val with 10 observers: 0.0007928750000000377s
10,000 updates to "name" string val with 100 observers: 0.0017347500000000195s
10,000 updates to "name" string val with 1000 observers: 0.007635916000000009s
100,000 updates to "name" string val with 10 observers: 0.01587337500000001s
100,000 updates to "name" string val with 100 observers: 0.020373334s
100,000 updates to "name" string val with 1000 observers: 0.12892133400000005s
1,000,000 updates to "name" string val with 10 observers: 0.14660462499999993s
1,000,000 updates to "name" string val with 100 observers: 0.20982341600000007s
1,000,000 updates to "name" string val with 1000 observers: 0.8595329590000002s

-----medium state object-----

1,000 updates to "user.settings.theme" string val with 10 observers: 0.0011529580000001261s
1,000 updates to "user.settings.theme" string val with 100 observers: 0.002206040999999914s
1,000 updates to "user.settings.theme" string val with 1000 observers: 0.006846457999999984s
10,000 updates to "user.settings.theme" string val with 10 observers: 0.01919616700000006s
10,000 updates to "user.settings.theme" string val with 100 observers: 0.007707957999999963s
10,000 updates to "user.settings.theme" string val with 1000 observers: 0.014114458000000014s
100,000 updates to "user.settings.theme" string val with 10 observers: 0.012900958000000174s
100,000 updates to "user.settings.theme" string val with 100 observers: 0.018197167000000035s
100,000 updates to "user.settings.theme" string val with 1000 observers: 0.09116387499999973s
1,000,000 updates to "user.settings.theme" string val with 10 observers: 0.18448416599999973s
1,000,000 updates to "user.settings.theme" string val with 100 observers: 0.24980879200000028s
1,000,000 updates to "user.settings.theme" string val with 1000 observers: 0.8701055419999998s

-----deep state object-----

1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.0002052080000003116s
1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.0002497920000000704s
1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.001332209000000148s
10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.0018820000000000618s
10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.0024639170000000377s
10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.014082249999999932s
100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.021280209000000015s
100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.02714695800000027s
100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.09046704200000022s
1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.2687363329999998s
1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.3244868329999999s
1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.9486893330000002s

Intel i9-13900HX · 32 GB RAM · Windows 11

-----shallow state object-----

1,000 updates to "name" string val with 10 observers: 0.0046989000000000015s
1,000 updates to "name" string val with 100 observers: 0.0009328000000000003s
1,000 updates to "name" string val with 1000 observers: 0.0012964000000000055s
10,000 updates to "name" string val with 10 observers: 0.002852000000000004s
10,000 updates to "name" string val with 100 observers: 0.003517799999999994s
10,000 updates to "name" string val with 1000 observers: 0.012490899999999996s
100,000 updates to "name" string val with 10 observers: 0.0199987s
100,000 updates to "name" string val with 100 observers: 0.03123379999999999s
100,000 updates to "name" string val with 1000 observers: 0.1196829s
1,000,000 updates to "name" string val with 10 observers: 0.2171915s
1,000,000 updates to "name" string val with 100 observers: 0.24709929999999997s
1,000,000 updates to "name" string val with 1000 observers: 1.1660891999999998s

-----medium state object-----

1,000 updates to "user.settings.theme" string val with 10 observers: 0.0007828999999999269s
1,000 updates to "user.settings.theme" string val with 100 observers: 0.0004992999999999483s
1,000 updates to "user.settings.theme" string val with 1000 observers: 0.0013275000000001s
10,000 updates to "user.settings.theme" string val with 10 observers: 0.0032917999999999667s
10,000 updates to "user.settings.theme" string val with 100 observers: 0.004183099999999967s
10,000 updates to "user.settings.theme" string val with 1000 observers: 0.012704099999999927s
100,000 updates to "user.settings.theme" string val with 10 observers: 0.023384700000000067s
100,000 updates to "user.settings.theme" string val with 100 observers: 0.03393959999999993s
100,000 updates to "user.settings.theme" string val with 1000 observers: 0.1242770999999998s
1,000,000 updates to "user.settings.theme" string val with 10 observers: 0.23742110000000002s
1,000,000 updates to "user.settings.theme" string val with 100 observers: 0.26894860000000015s
1,000,000 updates to "user.settings.theme" string val with 1000 observers: 1.2531396s

-----deep state object-----

1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.0003414999999999964s
1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.00033490000000028884s
1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.0016161000000001877s
10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.0022989000000002307s
10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.0037910000000001675s
10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.014288300000000163s
100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.025205199999999876s
100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.03601400000000012s
100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.13781609999999955s
1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.27891269999999985s
1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.36865729999999985s
1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 1.3965289000000003s