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

ripplio

v0.0.2

Published

An experimental, fine-grained reactive state manager for React — **heavily inspired by [Valtio]** — with:

Readme

ripplio (experimental)

An experimental, fine-grained reactive state manager for React — heavily inspired by Valtio — with:

  • Framework-agnostic core (no React imports, no classes)
  • Computed getters (including computed-of-computed)
  • Fine-grained subscriptions so components re-render only for what they read
  • React binding via useSnapshot built on useSyncExternalStore
  • Valtio-ish API (not drop-in compatible)

⚠️ Status: pre-alpha / research project. Expect breaking changes, edge cases, and incomplete features. Not production-ready.


Install

npm i ripplio
# or
pnpm add ripplio
# or
yarn add ripplio
  • Core is framework-agnostic and ships TypeScript types.
  • React binding lives under a subpath export.
// Core:
import { proxy, snapshot, ref } from 'ripplio';

// React hook:
import { useSnapshot } from 'ripplio/react';

Peer deps: React 18+ for the React binding. The core has no React dependency.


Quick start

1) Create a store (proxy)

import { proxy } from 'ripplio';

export const state = proxy({
  cart: {
    items: [
      { id: 'a', name: 'Widget', price: 10, qty: 1 },
      { id: 'b', name: 'Gadget', price: 20, qty: 2 },
    ],
  },

  taxRate: 0.1,

  // Computed chain
  get subtotal() {
    return this.cart.items.reduce((s, it) => s + it.price * it.qty, 0);
  },
  get tax() {
    return this.subtotal * this.taxRate;
  },
  get total() {
    return this.subtotal + this.tax;
  },
});

2) Read in React with useSnapshot

import { useSnapshot } from 'ripplio/react';
import { state } from './state';

export function Totals() {
  const snap = useSnapshot(state);
  return (
    <div>
      <div>Subtotal: {snap.subtotal}</div>
      <div>Tax: {snap.tax}</div>
      <div>Total: {snap.total}</div>
    </div>
  );
}

3) Mutate directly

function TaxControl() {
  const { taxRate } = useSnapshot(state); // read
  return (
    <div>
      <span>Tax: {taxRate}</span>
      <button onClick={() => (state.taxRate = +(Math.min(0.25, taxRate + 0.01).toFixed(2)))}
      >
        +0.01
      </button>
      <button onClick={() => (state.taxRate = +(Math.max(0, taxRate - 0.01).toFixed(2)))}
      >
        -0.01
      </button>
    </div>
  );
}

Fine-grained list rendering

Ripplio returns plain arrays from snapshots, so React rendering is predictable. Objects within are read via a “view” that yields plain primitives and tracks deps.

import { useSnapshot } from 'ripplio/react';
import { state } from './state';

function ItemsHeader() {
  const { cart } = useSnapshot(state);
  return <h3>Items ({cart.items.length})</h3>;
}

function ItemsList() {
  const { cart } = useSnapshot(state);
  return (
    <ul>
      {cart.items.map((_, i) => (
        <ItemRow key={state.cart.items[i].id} index={i} />
      ))}
    </ul>
  );
}

function ItemRow({ index }: { index: number }) {
  const row = useSnapshot(state.cart.items[index]);
  return (
    <li>
      {row.name} — ${row.price.toFixed(2)} × {row.qty}{' '}
      <button onClick={() => state.cart.items[index].qty++}>+1 qty</button>
    </li>
  );
}

Only the row you interact with re-renders; the header re-renders when items.length changes; totals re-render when fields used in the computed chain change.


Todo example (controlled inputs)

import { proxy } from 'ripplio';
import { useSnapshot } from 'ripplio/react';

const todos = proxy({
  list: [] as { text: string; done: boolean }[],
  get completed() {
    return this.list.filter((t) => t.done);
  },
  get remaining() {
    return this.list.filter((t) => !t.done);
  },
});

export function TodoApp() {
  const snap = useSnapshot(todos);

  return (
    <div>
      <button onClick={() => todos.list.push({ text: `Task ${todos.list.length + 1}`, done: false })}>
        Add Task
      </button>

      <ul>
        {snap.list.map((t, i) => (
          <li key={i}>
            <label>
              <input
                type="checkbox"
                checked={Boolean(t.done)}           {/* keep it controlled */}
                onChange={(e) => (todos.list[i].done = e.target.checked)}
              />
              {t.text}
            </label>
          </li>
        ))}
      </ul>

      <p>
        Completed: {snap.completed.length}, Remaining: {snap.remaining.length}
      </p>
    </div>
  );
}

Snapshots always expose arrays as plain arrays and their elements as plain objects with primitive fields, so checked is a boolean at first render.


API

Core (ripplio)

  • proxy<T extends object>(initial: T): T
    Wraps an object and returns a reactive proxy. Mutate it directly.

  • snapshot<T>(value: T): Snapshot<T>
    Produces a render-safe view:

    • Arrays → plain arrays (deep materialized)
    • Objects → lightweight “view” proxies; property reads return plain primitives and track deps
    • Safe to pass to JSX
  • ref<T>(value: T): Ref<T>
    Wrap a value to keep reference semantics (e.g., as a Map key) without further proxying.

Internals (not part of public API, but present): getStoreFor, withComponentTracking.

React (ripplio/react)

  • useSnapshot<T extends object>(state: T): Snapshot<T>
  • useSnapshot<T extends object, S>(state: T, selector: (state: T) => S): S (optional)

useSnapshot subscribes the component to exactly what it reads during render (via useSyncExternalStore). A small cache satisfies React’s getSnapshot stability requirement.


Computed values

Use ES getters. Computeds can depend on other computeds.

const s = proxy({
  items: [{ price: 10, qty: 1 }, { price: 20, qty: 2 }],
  taxRate: 0.1,
  get subtotal() {
    return this.items.reduce((sum, it) => sum + it.price * it.qty, 0);
  },
  get tax() {
    return this.subtotal * this.taxRate; // computed-of-computed
  },
  get total() {
    return this.subtotal + this.tax;
  },
});

How it works (high level)

  • Each computed has a key __computed__:path.
  • On read, it recomputes lazily if dirty and captures raw deps (like cart.items.0.qty) and prefixes (cart.items.0, cart.items, …).
  • A reverse index (raw→computed) and a computed→computed graph allow fast dirty propagation.
  • Components subscribe to either raw paths or computed keys, so updates precisely re-render the right views.

Differences from Valtio

  • Similar: direct mutations, useSnapshot, ref, getter-based computed values.
  • Different:
    • Not drop-in compatible.
    • Snapshots always give plain arrays (Valtio snapshots are deep frozen structures).
    • Some Valtio utilities (proxyMap, proxySet, etc.) are not implemented here (yet).

Tips & caveats

  • Always render from the snapshot, not the raw proxy.
  • Treat snapshots as read-only; mutate the proxy.
  • Use ref() for identity-sensitive keys you don’t want proxied.
  • Controlled inputs: checked={Boolean(t.done)} is a good defensive pattern.
  • This is experimental; expect sharp edges.

TypeScript

Fully typed. useSnapshot returns a typed snapshot view:

  • Primitives are primitives
  • Arrays are ReadonlyArray<...>
  • Objects expose the same keys but values are snapshot-safe (primitives or nested views)

Contributing

Issues and PRs welcome — just remember this is research code. Please describe the scenario, include a minimal repro, and note your React/Node/TS versions.


License

MIT