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

@streetui/state

v2.1.0

Published

StreetUI reactive state — signals, subscriptions, derived state

Readme

@streetui/state

StreetUI's reactive core: signals, derived values, effects, batching, stores — and resource(), the framework-native primitive for async data.

This document focuses on resource() and its error-handling companion, errorBoundary (which lives in @streetui/dsl because it is a DSL block). For the signal primitives (signal, derived, effect, batch) see the inline docs in src/signal.ts.

Why a resource primitive

Async data has the same three hard problems in every app: state (is it loading, did it fail, what's the value?), races (a slow request must not overwrite a newer one), and lifecycle (a request must stop touching the UI once its owner is gone). resource() solves all three on top of StreetUI's existing signals — no second reactive system, no virtual DOM, and no HTTP client baked in. The loader is any Promise-returning function, so fetch() works directly and any transport is supported.

resource(loader, options?)

import { resource } from '@streetui/state';

const users = resource<User[]>(
  ({ signal }) => fetch('/api/users', { signal }).then((r) => {
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    return r.json() as Promise<User[]>;
  }),
);

resource<T>(...) returns a strongly typed Resource<T> — data is T | undefined, and error is unknown (never any).

Loader

type ResourceLoader<T> = (ctx: { signal: AbortSignal }) => Promise<T> | T;

The loader receives an AbortSignal. Forward it to fetch (or any signal-aware operation) so in-flight work is cancelled when the resource is superseded or disposed. Cancellation is opt-in: ignoring the signal is fine, it just means the request runs to completion (its late result is discarded, never applied).

State (all reactive signals)

| Member | Type | Meaning | |---|---|---| | status | ReadonlySignal<'idle' \| 'loading' \| 'success' \| 'error'> | Lifecycle state | | data | ReadonlySignal<T \| undefined> | Latest successful value | | error | ReadonlySignal<unknown> | Latest error (undefined when none) | | loading | ReadonlySignal<boolean> | status === 'loading' | | isRefetching | ReadonlySignal<boolean> | Loading while previous data is retained |

These are read-only views over internal signals — consumers cannot .set them. Because they are ordinary signals, when(...), listOf(...), derived(...) and effects all work with them unchanged.

Refetching is not a separate status. A refetch enters status === 'loading' but keeps the previous data; use isRefetching (or loading && data !== undefined) to show a "refreshing" affordance without hiding the current data.

Methods

  • refetch(): Promise<void> — start a new request. The previous data is preserved during the reload (and on failure), so the UI never flashes empty.
  • dispose(): void — abort any in-flight request, drop watch subscriptions, and permanently stop the resource from writing state. Idempotent.

Options

interface ResourceOptions {
  immediate?: boolean;                              // default true — load on creation
  watch?: ReadonlyArray<ReadonlySignal<unknown>>;   // explicit dependencies
  onCleanup?: (fn: () => void) => void;             // auto-register dispose
}
  • immediate — set false to stay 'idle' until the first refetch().
  • watch — dependencies are explicit. When any listed signal changes, the resource refetches. There is no hidden auto-tracking, so there is no way to create an accidental infinite refetch loop.
  • onCleanup — pass a route's ctx.onCleanup (or any registrar) and dispose is registered for you.
const query = signal('');
const results = resource(
  ({ signal }) => search(query.get(), signal),
  { watch: [query], onCleanup: ctx.onCleanup },
);

Guarantees

  • Race safety. Each run gets a monotonic id; when a response arrives its id is compared to the latest. An older response can never overwrite a newer one.
  • Abort. Every run creates an AbortController; superseding a run or disposing the resource aborts the previous one. A self-triggered AbortError is swallowed (it is not a real failure and is never surfaced as error).
  • Lifecycle. After dispose() no further state writes happen — a late resolution or rejection is ignored. Wire dispose to the owner's cleanup and the resource can never update detached DOM.

Error boundaries (errorBoundary, from @streetui/dsl)

errorBoundary contains failures to a region of the tree instead of letting one failed async area take down the app:

c.errorBoundary('users', (body) => {
  body.when(users.loading, (l) => l.text('Loading…'));
  body.listOf('list', derived(() => users.data.get() ?? []), (u, _i, x) =>
    x.text(u.name),
  );
}, {
  source: users.error,                     // one signal or an array of them
  onRetry: () => void users.refetch(),
  fallback: (fb, error, retry) => {
    fb.text(`Failed: ${(error as Error).message}`);
    fb.button('Retry', { onClick: retry });
  },
});

Behaviour:

  • Enters the error state when any observed source signal becomes non-null, or when the body builder throws synchronously while building.
  • retry() clears the local error, invokes onRetry (typically resource.refetch), and re-attempts the body — without rebuilding the app.
  • Reuses the same reactive when() machinery, so the fallback subtree and every handler/subscription inside it is torn down on removal.
  • It does not trap arbitrary global errors, and errors remain observable via the resource's error signal.

SSR seeding

For server rendering, resource() accepts a server-resolved result so the client does not re-fetch on hydration:

const users = resource(loadUsers, {
  initialData: seed,        // from the SSR state island (see @streetui/renderer)
  // initialError: err,     // or seed an error instead
  // initialStatus: 'success',
});

When any of initialData / initialError / initialStatus is provided, the resource starts in the corresponding non-idle state ('success' from initialData, 'error' from initialError, or an explicit initialStatus) and skips the automatic initial load. It can still refetch() on demand; pass immediate: true if you deliberately want a client-side refetch right after hydration. Read the seed out of the SSR island with readState from @streetui/renderer.

What this is not

resource() is deliberately small. It is not a caching layer, query library, mutation framework, global store, suspense integration, or devtools. Those are separate concerns that can be composed on top if and when they are needed.