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

@bgub/fig

v2.0.0

Published

Fig — a UI library

Readme

@bgub/fig

Core primitives for Fig, a TypeScript re-implementation of React's modern component model. Renderers live in @bgub/fig-dom and @bgub/fig-server.

Installation

pnpm add @bgub/fig

Fig packages are ESM-only, require Node ^20.19.0 || >=22.12.0 for Node runtime entry points, and rely on package exports. TypeScript projects should use moduleResolution: "bundler", "node16", or "nodenext", not the legacy "node" resolver, so subpaths such as @bgub/fig/jsx-runtime resolve with their types.

Usage

import { createRoot, on } from "@bgub/fig-dom";
import { Suspense, readPromise, useState } from "@bgub/fig";

const message = Promise.resolve("Ready");

function Message({ value }: { value: Promise<string> }) {
  return <span>{readPromise(value)}</span>;
}

function App() {
  const [count, setCount] = useState(0);

  return (
    <main>
      <button mix={on("click", () => setCount((value) => value + 1))}>
        Count {count}
      </button>
      <Suspense fallback={<span>Loading</span>}>
        <Message value={message} />
      </Suspense>
    </main>
  );
}

const container = document.getElementById("root");
if (container === null) throw new Error("Missing root.");

createRoot(container).render(<App />);

Core API

  • Elements: createElement, isValidElement, Fragment, and the JSX runtime.

  • Host behavior: createMixin(type) creates render-time prop composers for intrinsic elements. A host accepts one descriptor or nested arrays through mix; mixins may return props or more mixins but cannot replace children, key, or unsafeHTML.

  • State and memoization: useState, useDeferredValue, useMemo, and useCallback.

  • Stable identifiers: useId() generates IDs that match server render and hydration output.

  • Context: createContext(defaultValue) plus render-time readContext(context).

  • Effects: useReactive, useBeforePaint, and useBeforeLayout. Effects receive an AbortSignal; attach cleanup to signal.abort. An empty deps array runs an effect once per mount.

  • Strict development semantics, with no StrictMode component or opt-out: development builds render components twice per pass (discarding the first result) and run first-time effects and renderer bind callbacks twice with an abort in between, so impure renders and signal-ignoring cleanup surface early. Production builds strip these checks.

  • useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?) reads external stores. Server rendering and hydration require getServerSnapshot.

  • useStableEvent(handler) returns a stable function for effect-held callbacks (subscriptions, sockets, timers). The handler always sees the latest committed render and follows the Fig event contract: it receives a trailing AbortSignal, and the previous invocation's signal aborts on re-entry and on unmount. Calls after unmount receive an already-aborted signal; calling it during render throws.

  • Suspense catches pending readPromise(promise) reads and shows fallback until the promise settles.

  • Data resources live in @bgub/fig. Use dataResource(...) plus render-time readData(...) for keyed async values that need Suspense, deduping, invalidation, refresh, server hydration, and DevTools visibility.

  • lazy(load) creates a component that suspends until load() resolves to a component type.

  • <Activity mode="visible" | "hidden"> hides a subtree while preserving its state: hiding hides the DOM (through portals) and aborts effects, binds, and reactive events; revealing restores the DOM and re-runs them. Trees that mount hidden defer their effects until first reveal, and updates inside hidden trees prerender at idle priority. Server rendering streams hidden content inside an inert template; the client keeps it dehydrated — zero hydration cost — until reveal, then adopts the server DOM.

  • ErrorBoundary catches render and Fig effect errors. Use onError for reporting and change the boundary key to reset sticky fallback state. Data resource load failures report failed keys on info.dataResourceKeys, so recovery flows can refresh or invalidate those keys before resetting. This includes a streamed payload hole that rejects after its owning entry has already fulfilled.

  • transition(callback) and useTransition() mark updates that may preserve already-revealed Suspense content while new work is pending. Updates scheduled inside the callback run at transition priority, including updates after an await while an async transition callback is still pending. If the callback returns a promise, useTransition() keeps isPending true until it settles. Server rendering runs transition callbacks immediately and never exposes pending state.

  • useActionState(action, initialState) matches React's argument order while staying client-side in Fig today. Actions receive the previous state first, then the runner's arguments, then an AbortSignal Fig appends — declare the trailing signal parameter (it drives Args inference):

    const [count, add, isPending] = useActionState(
      (previous: number, amount: number, signal: AbortSignal) => {
        return fetchNext(previous + amount, { signal });
      },
      0,
    );
    add(2); // Fig appends the signal; callers pass only the args.

    Runs are last-run-wins: a new run aborts the previous one's signal and retires it, so a stale settlement (value or rejection) never touches state, error, or pending. The signal also aborts on unmount and Activity hide. Server actions can layer on top later without changing the hook shape.

  • Document resources: assets([...], children) attaches resources to a subtree while rendering only children on the client. Resource helpers include stylesheet, preload, font, preconnect, title, meta, and script. Server rendering exposes title and meta as document head output; stream-safe assets such as stylesheets can be emitted near the segments that depend on them.

Renderer APIs

Use @bgub/fig-dom for browser rendering:

  • createRoot(container) renders client roots.
  • hydrateRoot(container, children, options?) hydrates server HTML and reports recoverable mismatches with onRecoverableError.
  • createPortal(children, container, key?) renders into external DOM targets.
  • DOM events use mix={on("click", (event, signal) => ...)}; arrays compose multiple or conditional mixins.
  • DOM node access uses bind={(node, signal) => ...}.
  • Raw trusted HTML uses unsafeHTML="<p>trusted html</p>".

Use @bgub/fig-server for streaming server rendering with renderToStream or renderToHtml.

Data Resources

Key-addressable async data resources are part of the core package. Data resources are render-time inputs with stable keys: Fig dedupes loads by key, wires missing values to Suspense, tracks committed readers, and lets applications invalidate or refresh specific entries and key prefixes. The store implementation travels with dataResource itself, so bundles that never define a resource never ship it.

Basic Usage

import { Suspense } from "@bgub/fig";
import { createRoot, on } from "@bgub/fig-dom";
import { dataResource, readData, refreshData } from "@bgub/fig";

const userResource = dataResource({
  key: (id: string) => ["user", id],
  load: async (id: string, { signal }) => {
    const response = await fetch(`/api/users/${id}`, { signal });
    return response.json() as Promise<{ name: string }>;
  },
});

function Profile({ id }: { id: string }) {
  const user = readData(userResource, id);

  return (
    <button mix={on("click", () => void refreshData(userResource, id))}>
      {user.name}
    </button>
  );
}

createRoot(document.getElementById("root")!).render(
  <Suspense fallback={<span>Loading</span>}>
    <Profile id="one" />
  </Suspense>,
);

readData(resource, ...args) is render-only. It returns fulfilled values, throws pending loads to Suspense, and throws failed initial loads to ErrorBoundary.

Keys

Every resource has a key function. Keys must be arrays whose first item is a string namespace:

key: (id: string, locale: string) => ["user", id, { locale }];

Key values may contain strings, finite numbers, booleans, null, arrays, and plain objects. Object keys are canonicalized in sorted order. undefined, NaN, infinities, functions, symbols, dates, maps, sets, promises, class instances, and cycles are invalid.

The key is the whole cache identity. Fig uses the canonical key for reads, dedupe, invalidation, refresh, SSR serialization, hydration, diagnostics, and DevTools.

Refresh And Invalidate

invalidateData(resource, ...args) marks an existing entry stale. If the key is currently observed, Fig schedules the readers; the next read keeps showing the last fulfilled value and starts a background reload. If nobody is observing the key, invalidation is lazy and does not fetch.

invalidateDataKey(key) applies the same stale/reset semantics to one exact structured key. invalidateDataError(error) invalidates every exact key Fig attributed to a caught data error and returns whether any data keys were found. Use it from an ErrorBoundary fallback before remounting or resetting the boundary.

invalidateDataPrefix(prefix) marks every existing entry whose structured key starts with prefix stale. For example, invalidateDataPrefix(["user"]) targets entries like ["user", id]. Prefix matching uses the same canonical key encoder as normal reads, so delimiter-shaped strings do not collide.

refreshData(resource, ...args) fetches immediately and resolves with a result instead of rejecting:

const result = await refreshData(userResource, "one");

if (result.status === "fulfilled") {
  console.log(result.value);
}

Result statuses are fulfilled, rejected, aborted, and unsupported. Refresh failures preserve the last fulfilled value when one exists, so visible UI does not fall back to Suspense or an error boundary just because a refresh failed.

Root Store Scope

Fig installs the current root data store while rendering and while running Fig-managed callbacks such as delegated event handlers. Outside those scopes, bind the store explicitly:

const root = createRoot(container);

socket.on("user:update", (id) => {
  root.data.run(() => invalidateData(userResource, id));
});

root.data.run(fn) is synchronous. If you need to invalidate after an await, keep the handle and re-enter:

const data = root.data;
await save();
data.run(() => refreshData(userResource, "one"));

Framework adapters can create and populate a root-neutral store before a renderer exists, then let exactly one root adopt that same store:

const dataStore = createDataStore();

await dataStore.ensureData(userResource, "one");

const root = createRoot(container, { dataStore });
root.data === dataStore; // true

Adoption preserves entry identity and subscriptions. The adopting root owns the store lifetime and disposes it when the root unmounts.

Client roots accept dataStore, dataPartition, and initialData options. dataStore is exclusive with the other two options. The partition separates a store's internal keyspace without changing public resource keys. Loaders receive only their resource arguments plus { signal }; app services, request cookies, and auth state belong in the surrounding adapter or closure, not in the data store.

Server Values And Hydration

Server renderers expose fulfilled data entries with getData(). Pass those entries to the client root as initialData to hydrate by key, or use createDataStore() when an adapter needs to populate and render through the same live store.

For server-only data, split the browser-safe key resource from the server loader:

// user-data.ts
import { dataResource } from "@bgub/fig";

export const userKey = (id: string) => ["user", id];

export const userResource = dataResource<[string], User>({
  key: userKey,
});
// user-data.server.ts
import { dataResource } from "@bgub/fig";
import { userKey } from "./user-data.ts";

export const userServerResource = dataResource({
  key: userKey,
  load: async (id, { signal }) => fetchUser(id, signal),
});

Client code imports the loader-less resource. If the server streamed a value for the same key, the client can read it as a hydrate-only entry. Since the resource has no client loader, refreshData(userResource, id) resolves with { status: "unsupported", reason: "no-client-loader" }; revalidation needs a payload or framework refresh path.

Place the loader-backed definition behind the host framework's server-only module boundary. Browser code imports only the explicit loader-free resource; Fig joins the two definitions by key during hydration without a bundler transform. If only server-rendered code reads the value, the single loader-backed definition is sufficient.

Direct client refreshes of server data are a framework feature, not a core data one: an endpoint has to exist to serve them. Define an isomorphic dataResource whose loader calls the framework endpoint; with TanStack Start, compose it with createServerFn. Server route payload navigations do not make an extra data request either way — data read during the payload render streams in that same payload response.

Activity, Errors, And DevTools

Data-resource subscriptions are committed even inside hidden Activity trees. Invalidating a hidden-only key schedules offscreen prerender work and keeps the DOM hidden.

Initial load errors are keyed. ErrorBoundary reports the failed keys on info.dataResourceKeys; reset flows can call invalidateDataError(error) or refresh/invalidate those keys before remounting or changing the boundary key.

Fig DevTools root snapshots include data-resource entries with their normalized keys, statuses, stale state, subscriber counts, current values, and errors. The in-page DevTools exposes them in the Data tab.

License

MIT