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

@playgenx/components

v0.2.2

Published

Default React 19 implementations for every entry in DEFAULT_REGISTRY. Pair with @playgenx/registry and the parser/validator pipeline.

Readme

@playgenx/components

Default React 19 implementations for every entry in DEFAULT_REGISTRY. The components, a per-Provider reactive state store, error boundaries, and a CLI for inspecting state snapshots.

npm install @playgenx/components

React 19 is a peer dependency — your app provides it.

// package.json (the bits you need)
{
  "dependencies": {
    "@playgenx/components": "^0.2.1",
    "react": "^19.2.0",
    "react-dom": "^19.2.0"
  }
}

Looking for the renderer (parser + tree walker)? That's a separate package — @playgenx/renderer. This package is the component library; the renderer consumes it.


Quick start

import {
  Slider,
  PlaygroundStateProvider,
  usePlaygroundState,
} from '@playgenx/components';
import { useState } from 'react';

// 1. Standalone, no state — works without a Provider
export function StaticVolume() {
  return <Slider min={0} max={10} value={4} label="Volume" />;
}

// 2. With shared state — wrap your tree
export function VolumePanel() {
  return (
    <PlaygroundStateProvider initial={{ volume: 5 }}>
      <Slider min={0} max={10} stateKey="volume" label="Volume" />
      <VolumeReadout />
    </PlaygroundStateProvider>
  );
}

function VolumeReadout() {
  const state = usePlaygroundState();
  return <span>Current: {state.get<number>('volume')}</span>;
}

What's in the box

Components (PascalCase, React 19)

Every component accepts JSON-friendly props. See index.ts for the full surface.

| Component | Purpose | Notable props | |---------------|--------------------------------------------|--------------------------------| | Button | Headless button | label, variant, onClick | | TextField | Controlled / uncontrolled input | value, onChange | | Slider | Range input | min, max, value, stateKey | | Chart | bar / line / pie SVG charts | kind, data, title | | Container | Layout primitive | padding, gap | | Code | Inline code / <pre><code> | language | | Heading | <h1><h6> | level | | Text | <p> with weight + color | weight, color | | Stepper | Multi-step reveal | steps, initial | | Card | Titled card | title, children | | List | <ul> / <ol> from array | items, ordered |

State store + React glue

| Export | What it does | |------------------------------|-----------------------------------------------------------| | createPlaygroundState(initial?) | Create a standalone state store | | PlaygroundStateProvider | Wrap your tree with a Provider | | usePlaygroundState() | Read the nearest store | | useBoundValue(key, fb) | Get a live [value, setter] tuple (re-renders on change) | | useBoundValueOrUndefined(key, fb) | Same; returns undefined if no key/provider | | useStoreSnapshot() | Non-subscribing {get, set, subscribe, snapshot, ...} | | useBoundSelector(key, sel, fb) | Live bound value with selector transform; only re-renders on slice change | | useMultiBoundValue(bindings) | Bind multiple keys; returns Record<key, [value, setter]> | | useStateAction(action) | Build an onClick from {set, toggle} config |

Pure helpers (no React, no Provider)

| Export | Returns | |-----------------------|------------------------------------| | dumpState(state) | JSON v1 envelope (sorted keys) | | diffState(a, b) | {added, changed, removed} or null | | diffSnapshots(prev, next) | Same as diffState but pure | | validateStateKey(k) | null if valid, error message if not | | clearState(state) | (void) — sets every key to undefined | | batch(state, fn) | Atomic set commit, per-key dedup |

Error handling

| Export | Purpose | |------------------------------|------------------------------------| | ArtifactErrorBoundary | Class-component error boundary | | ShowSource | Render-prop source-toggle |

Registry

| Export | Purpose | |-------------------------|--------------------------------------| | componentMap | Frozen default registry (11 entries) | | createRegistry(overrides) | Returns a new frozen map |


Three ways to use the state store

1. Standalone, outside React

import { createPlaygroundState } from '@playgenx/components';

const state = createPlaygroundState({ count: 0 });
state.set('count', 5);
state.get('count');           // 5
state.snapshot();             // { count: 5 }
state.subscribe('count', (v) => console.log('count is now', v));
state.set('count', 10);       // → console.log fires on the next microtask

2. React tree with a Provider

import { PlaygroundStateProvider, useBoundValue } from '@playgenx/components';

function VolumeControl() {
  // Live-bound: writes to state on change, re-renders on external writes
  const [volume, setVolume] = useBoundValue<number>('volume', 50);
  return (
    <input
      type="range"
      min={0}
      max={100}
      value={volume}
      onChange={(e) => setVolume(Number(e.currentTarget.value))}
    />
  );
}

export function App() {
  return (
    <PlaygroundStateProvider initial={{ volume: 50 }}>
      <VolumeControl />
    </PlaygroundStateProvider>
  );
}

3. State-bound component (the simple path)

// No need to write your own useBoundValue — Slider + stateKey does it.
<PlaygroundStateProvider initial={{ volume: 50 }}>
  <Slider min={0} max={100} stateKey="volume" />
</PlaygroundStateProvider>

Flexibility patterns — pick the right hook for the job

v0.2.2 ships five hooks for binding components to state. They differ in what triggers a re-render, what they assume about Providers, and how they expose state to your code. Pick the lowest-cost one that fits.

| Hook | Subscribes? | Needs Provider? | SSR-safe? | Use when… | |------------------------|-------------|------------------|-----------|-----------------------------------------| | useBoundValue | yes | yes (throws) | yes | You need to re-render on state changes and a Provider is mandatory | | useBoundValueOrUndefined | yes | no (graceful) | yes | stateKey is optional; no Provider = no-op binding | | useStoreSnapshot | no | yes (throws) | n/a | You read/write state in handlers but don't want re-renders | | useBoundSelector | yes (filtered) | yes (throws) | yes | One key holds an object; you only care about one slice | | useMultiBoundValue | yes (1 per key) | yes (throws) | yes | A form/dashboard binds 5-10 keys; you want a Record |

useStoreSnapshot — for non-subscribing reads

When you need state in event handlers but don't want the component to re-render on every change:

import { useStoreSnapshot } from '@playgenx/components';

function SaveButton() {
  const { get, set, snapshot } = useStoreSnapshot();
  return (
    <button onClick={() => {
      // Read multiple keys atomically, write back, no re-render
      const form = snapshot();
      if (form.dirty) {
        set('savedAt', Date.now());
        api.save(form);
      }
    }}>
      Save
    </button>
  );
}

The hook returns { state, get, set, subscribe, snapshot, replaceAll } — all stable references for the Provider's lifetime.

useBoundSelector — slice-aware subscriptions

When one key holds an object and you only care about one field:

import { useBoundSelector } from '@playgenx/components';

interface User { name: string; age: number; email: string; }

function CurrentUserBadge() {
  // Only re-renders when `name` changes, not when `age` changes.
  const [name] = useBoundSelector<User, string>(
    'currentUser',
    (u) => u?.name ?? 'guest',
    'guest',
  );
  return <span>Hi, {name}!</span>;
}

Important: write selectors that return primitives or stable refs. A selector that returns a fresh object (u => ({...u})) defeats the slice-detection and re-renders on every store change.

useMultiBoundValue — bind many keys at once

For forms and dashboards where you have several bound inputs in one component:

import { useMultiBoundValue } from '@playgenx/components';

function SettingsForm() {
  const form = useMultiBoundValue({
    name:  { fallback: '' },
    email: { fallback: '' },
    age:   { fallback: 0 },
    role:  { fallback: 'user' },
  });
  return (
    <>
      <input value={form.name[0]}  onChange={e => form.name[1](e.target.value)} />
      <input value={form.email[0]} onChange={e => form.email[1](e.target.value)} />
      <input type="number" value={form.age[0]} onChange={e => form.age[1](+e.target.value)} />
      <select value={form.role[0]} onChange={e => form.role[1](e.target.value)}>
        <option value="user">User</option>
        <option value="admin">Admin</option>
      </select>
    </>
  );
}

The hook subscribes once per key (so 5 keys = 5 subscriptions). For 50+ rows of dynamic data, use useStoreSnapshot + manual get/set calls instead — the hook count is bounded.

SSR — state is reflected in the first render

All useBoundValue* hooks use React 19's useSyncExternalStore so the server-rendered HTML matches the live store value, not a fallback. renderToString(<Slider stateKey="v" />) inside a Provider with initial={{ v: 7 }} produces <input value="7" ... /> — no hydration mismatch.

// Server
import { renderToString } from 'react-dom/server';
import { Slider, PlaygroundStateProvider } from '@playgenx/components';

const html = renderToString(
  <PlaygroundStateProvider initial={{ volume: 50 }}>
    <Slider min={0} max={100} stateKey="volume" />
  </PlaygroundStateProvider>
);
// html contains `value="50"` — not `value="0"` (the local fallback)

If your framework has a renderToReadableStream or similar, the same guarantee holds. The hook count is 3 (down from 5 in 0.2.1) thanks to useSyncExternalStore collapsing the previous useState + useState + useEffect trio.


Slider — the three operating modes

The Slider component accepts value and/or stateKey. They mean different things:

| Pass | Mode | Behavior | |---------------------|--------------------------|---------------------------------------| | Neither | Uncontrolled | Owns its own local state | | value={50} only | Controlled, read-only | User input is ignored (no onChange wired) | | stateKey="volume" | Bound to state | Reads/writes via the Provider |

The bound mode wins when both are present. value is clamped to [min, max] on read; NaN clamps to min; min === max disables the slider.


The CLI: playgenx-state

After npm install @playgenx/components, you get a playgenx-state binary on your PATH (via npm's bin field). It operates on JSON snapshots on stdin/stdout.

# Pretty-print a snapshot in the v1 envelope format
$ echo '{"volume":5,"user":{"name":"a"}}' | playgenx-state snapshot
{
  "version": 1,
  "keys": [
    "user",
    "volume"
  ],
  "values": {
    "volume": 5,
    "user": { "name": "a" }
  }
}

# Validate stateKey strings before using them in a body
$ echo '["good","bad key","has/slash"]' | playgenx-state validate
[
  { "key": "good", "ok": true },
  { "key": "bad key", "ok": false, "error": "stateKey must not contain whitespace" },
  { "key": "has/slash", "ok": false, "error": "stateKey contains forbidden characters..." }
]
# exit code 3 (validation failed)

# Diff two snapshots
$ echo '{"prev":{"a":1},"next":{"a":2,"b":3}}' | playgenx-state diff
{ "added": ["b"], "changed": ["a"], "removed": [] }

# Inspect keys / count
$ echo '{"b":1,"a":2,"c":3}' | playgenx-state keys
[ "a", "b", "c" ]
$ echo '{"b":1,"a":2,"c":3}' | playgenx-state count
{ "keys": 3 }

Exit codes: 0 ok, 1 unknown subcommand, 2 bad input, 3 validation failure.

These are the same helpers exported from the package as library functions (dumpState, diffSnapshots, validateStateKey, etc.) — the CLI is a thin wrapper. Use whichever fits.


Architecture notes

  • Per-Provider scope. Each PlaygroundStateProvider creates an independent store. Nested Providers create nested scopes (a child Provider does not see the parent's keys).
  • HMR-safe. Subscribers are cleared on Provider unmount, so a hot-reload that re-mounts the Provider doesn't leak subscribers to the old store.
  • Microtask batched. set() schedules subscriber fires on the next microtask. Multiple sets in the same tick produce one fire per affected key. Use batch(state, fn) if you want each subscriber to see only the LAST value set in a group.
  • No deep clone. Values are stored by reference. Don't mutate objects after set()-ing them — use a fresh object instead.
  • Object.is equality. Setting a value that Object.is-equals the current value is a no-op (no subscriber fire, no flush). This means set('x', NaN) twice fires once, and set('x', undefined) on an unset key is a no-op (since Object.is(undefined, undefined) is true).
  • Sub-Provider remount drops old subscribers. Subscribers from a unmounted Provider's tree do not fire for sets on a NEW Provider's store, even with the same key name. (Verified by the A.7 hardening test.)

TypeScript

This package is written in TypeScript with strict types. The dist/index.d.mts declarations are exhaustive — you can import type { SliderProps, PlaygroundState, StateDiff } etc.

Common imports:

import type {
  SliderProps,
  ButtonProps,
  PlaygroundState,
  StateAction,
  StateDiff,
  StateEnvelope,
  RenderBodyOptions,
} from '@playgenx/components';

Versioning

  • 0.1.x — Component-only, no state store. Slider had no stateKey prop.
  • 0.2.0 — Adds PlaygroundStateProvider, useBoundValue*, useStateAction, ArtifactErrorBoundary, ShowSource, createRegistry, the pure helpers (dumpState etc.), and the playgenx-state CLI binary. The stateKey prop on Slider is fully wired and uses the new state store.
  • 0.2.1 — Exposes the CLI binary via the package's bin field so npx playgenx-state works after a fresh install.
  • 0.2.2 — SSR fix: useBoundValueOrUndefined now uses React 19's useSyncExternalStore so renderToString reflects the Provider's live value (no more "fallback on server, real value after hydration" mismatch). Adds three flexibility hooks: useStoreSnapshot (non-subscribing), useBoundSelector (slice-aware subscription), useMultiBoundValue (batch key binding). 108 tests passing.

License

MIT — same as the parent project.