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

@wcstack/fetch

v1.15.0

Published

Declarative fetch component for Web Components. Framework-agnostic async data fetching via wc-bindable-protocol.

Readme

@wcstack/fetch

@wcstack/fetch is a headless fetch component for the wcstack ecosystem.

It is not a visual UI widget. It is an I/O node that connects HTTP requests to reactive state.

With @wcstack/state, <wcs-fetch> can be bound directly through path contracts:

  • input / command surface: url, body, trigger
  • output state surface: value, loading, error, status

This means async communication can be expressed declaratively in HTML, without writing fetch(), async/await, or loading/error glue code in your UI layer.

@wcstack/fetch follows the CSBC (Core / Shell / Binding Contract) architecture:

  • Core (FetchCore) handles HTTP, abort, and async state
  • Shell (<wcs-fetch>) connects that state to the DOM
  • Binding Contract (static wcBindable) declares observable properties, writable inputs, and callable commands

Why this exists

In many frontend apps, the hardest part to migrate is not the template — it is the async logic: HTTP requests, loading flags, errors, retries, and lifecycle cleanup.

@wcstack/fetch moves that async logic into a reusable component and exposes the result as bindable state.

With @wcstack/state, the flow becomes:

  1. state computes url
  2. <wcs-fetch> executes the request
  3. async results return as value, loading, error, status
  4. UI binds to those paths with data-wcs

This turns async communication into state transitions, not imperative UI code.

Install

npm install @wcstack/fetch

Quick Start

1. Reactive fetch from state

When url changes, <wcs-fetch> automatically runs a new request. If another request is already in flight, it aborts the previous one.

<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
<script type="module" src="https://esm.run/@wcstack/fetch/auto"></script>

<wcs-state>
  <script type="module">
    export default {
      users: [],
      get usersUrl() {
        return "/api/users";
      },
    };
  </script>
</wcs-state>

<wcs-fetch data-wcs="url: usersUrl; value: users"></wcs-fetch>

<ul>
  <template data-wcs="for: users">
    <li data-wcs="textContent: users.*.name"></li>
  </template>
</ul>

This is the default mode:

  • connect url
  • receive value
  • optionally bind loading, error, and status

2. Reactive URL example

A computed URL can drive data fetching automatically:

<wcs-state>
  <script type="module">
    export default {
      filterRole: "",
      users: [],

      get usersUrl() {
        const role = this.filterRole;
        return role ? "/api/users?role=" + role : "/api/users";
      },
    };
  </script>
</wcs-state>

<select data-wcs="value: filterRole">
  <option value="">All</option>
  <option value="admin">Admin</option>
  <option value="staff">Staff</option>
</select>

<wcs-fetch
  data-wcs="url: usersUrl; value: users; loading: listLoading; error: listError">
</wcs-fetch>

<template data-wcs="if: listLoading">
  <p>Loading...</p>
</template>
<template data-wcs="if: listError">
  <p>Failed to load users.</p>
</template>

<ul>
  <template data-wcs="for: users">
    <li data-wcs="textContent: users.*.name"></li>
  </template>
</ul>

3. Manual execution with trigger

Use manual when you want to prepare inputs first and execute later.

<wcs-state>
  <script type="module">
    export default {
      users: [],
      shouldRefresh: false,

      reload() {
        this.shouldRefresh = true;
      },
    };
  </script>
</wcs-state>

<wcs-fetch
  url="/api/users"
  manual
  data-wcs="trigger: shouldRefresh; value: users; loading: listLoading">
</wcs-fetch>

<button data-wcs="onclick: reload">Refresh</button>

trigger is a one-way command surface:

  • writing true starts fetch()
  • it resets itself to false after completion
  • the reset emits wcs-fetch:trigger-changed
external write:  false → true   No event (triggers fetch)
auto-reset:      true  → false  Dispatches wcs-fetch:trigger-changed

If url is empty when true is written (e.g. a state-driven computed url not yet resolved), the write is silently ignored: no fetch runs, trigger stays false, and no event fires. Set the url first, then write true again to execute.

4. POST with reactive body

<wcs-state>
  <script type="module">
    export default {
      newUser: {
        name: "",
        email: "",
      },
      submitRequest: false,
      submitResult: null,
      submitError: null,

      submit() {
        this.submitRequest = true;
      },
    };
  </script>
</wcs-state>

<input data-wcs="value: newUser.name" placeholder="Name">
<input data-wcs="value: newUser.email" placeholder="Email">

<button data-wcs="onclick: submit">Create</button>

<wcs-fetch
  url="/api/users"
  method="POST"
  manual
  data-wcs="
    body: newUser;
    trigger: submitRequest;
    value: submitResult;
    error: submitError;
    loading: submitLoading
  ">
  <wcs-fetch-header name="Content-Type" value="application/json"></wcs-fetch-header>
</wcs-fetch>

<template data-wcs="if: submitLoading">
  <p>Submitting...</p>
</template>
<template data-wcs="if: submitError">
  <p>Submit failed.</p>
</template>

5. Infinite scroll with <wcs-infinite-scroll>

<wcs-infinite-scroll> runs an existing <wcs-fetch> when its sentinel element enters the viewport. Keep page numbers, next URLs, and response append behavior in @wcstack/state; this tag only owns scroll detection.

Behavior rules:

  • it does not trigger again while the target <wcs-fetch> is loading
  • once is strict: after the first execution, changing attributes does not re-arm observation
  • if target does not resolve, or resolves to a non-<wcs-fetch> element, it is a silent no-op
<wcs-state>
  <script type="module">
    export default {
      page: 1,
      users: [],
      get nextUsersUrl() {
        return "/api/users?page=" + this.page;
      },
    };
  </script>
</wcs-state>

<wcs-fetch
  id="next-page-fetch"
  manual
  data-wcs="url: nextUsersUrl; loading: listLoading; error: listError">
</wcs-fetch>

<ul>
  <template data-wcs="for: users">
    <li data-wcs="textContent: users.*.name"></li>
  </template>
</ul>

<wcs-infinite-scroll
  target="next-page-fetch"
  root-margin="240px 0px">
</wcs-infinite-scroll>

Attributes:

  • target: id of the <wcs-fetch> to run
  • root: id of the scroll container. Defaults to the viewport
  • root-margin: preload distance. Passed to IntersectionObserver.rootMargin
  • threshold: intersection threshold. Defaults to 0
  • disabled: stops observing
  • once: disconnects after the first execution and does not re-arm afterwards

State Surface vs Command Surface

<wcs-fetch> exposes two different kinds of properties.

Output state (bindable async state)

These properties represent the result of the current request and are the main observable surface:

| Property | Type | Description | |----------|------|-------------| | value | any | Response data. Reset to null on HTTP error (status >= 400) | | loading | boolean | true while a request is in flight | | error | WcsFetchHttpError \| Error \| null | HTTP or network error | | status | number | HTTP status code |

Note: On an HTTP error, value is reset to null and status carries the error code. If you bind only value (without observing error), the previous successful value disappears when a request fails. Bind error to handle the failure case explicitly.

Input / command surface

These properties control request execution from HTML, JS, or @wcstack/state bindings:

| Property | Type | Description | |----------|------|-------------| | url | string | Request URL | | body | any | Request body (resets to null after fetch()) | | trigger | boolean | One-way execution trigger | | manual | boolean | Disables auto-fetch on connect / URL change |

Architecture

@wcstack/fetch follows the CSBC architecture.

Core: FetchCore

FetchCore is a pure EventTarget class. It contains:

  • HTTP execution
  • abort control
  • async state transitions
  • wc-bindable-protocol declaration for observable state and callable commands

It can run headlessly in any runtime that supports EventTarget and fetch.

Shell: <wcs-fetch>

<wcs-fetch> is a thin HTMLElement wrapper around FetchCore. It adds:

  • attribute / property mapping
  • DOM lifecycle integration
  • declarative execution helpers such as trigger
  • wc-bindable-protocol inputs for DOM-facing configuration and command properties

This split keeps the async logic portable while allowing DOM-based binding systems such as @wcstack/state to interact with it naturally.

Target injection

The Core dispatches events directly on the Shell via target injection, so no event re-dispatch is needed.

Headless Usage (Core only)

FetchCore can be used standalone without the DOM. Since it declares static wcBindable, you can use @wc-bindable/core's bind() to subscribe to its state — the same way framework adapters work:

import { FetchCore } from "@wcstack/fetch";
import { bind } from "@wc-bindable/core";

const core = new FetchCore();

const unbind = bind(core, (name, value) => {
  console.log(`${name}:`, value);
});

await core.fetch("/api/users");

unbind();

This works in Node.js, Deno, Cloudflare Workers — anywhere EventTarget and fetch are available.

URL Observation

By default, <wcs-fetch> automatically executes a request when:

  1. it is connected to the DOM and url is set
  2. the url changes

If a request is already in flight when the URL changes, the previous request is automatically aborted before the new one starts.

Set the manual attribute to disable auto-fetch and control execution explicitly via fetch() or trigger.

Note (since v1.13): Auto-fetch is deferred to a microtask instead of firing synchronously. Multiple input writes in the same tick (e.g. a ...: spread writing url and manual in sequence) collapse into a single decision made against the final element state, and rewriting an unchanged url does not refetch. To await the connect-time fetch, use connectedCallbackPromise — reading promise synchronously right after appendChild returns the initial resolved promise, not the auto-fetch. Explicit triggers (fetch(), trigger, the fetch command) are unaffected and still run immediately.

Programmatic Usage

const fetchEl = document.querySelector("wcs-fetch");

// Set body via JS API (takes priority over <wcs-fetch-body>)
fetchEl.body = { name: "Tanaka" };
await fetchEl.fetch();
// Note: body is automatically reset to null after fetch().
// Set it again before each call if needed.

console.log(fetchEl.value);   // response data
console.log(fetchEl.status);  // HTTP status code
console.log(fetchEl.loading); // boolean
console.log(fetchEl.error);   // error info or null
console.log(fetchEl.body);    // null (reset after fetch)

HTML Replace Mode

<wcs-fetch> can also replace a target element's innerHTML when target is set.

<div id="content">Initial content</div>
<wcs-fetch url="/api/partial" target="content"></wcs-fetch>

This mode is useful for simple fragment loading, but it is separate from the main state-driven usage with @wcstack/state.

Security note: The response is assigned directly to targetElement.innerHTML without sanitization. Only use target with fragments from a trusted endpoint you control. Untrusted HTML can carry XSS payloads (e.g. event-handler attributes). For untrusted or user-influenced content, bind value into state and render through @wcstack/state text bindings instead.

Optional DOM Triggering

If autoTrigger is enabled (default), clicking an element with data-fetchtarget triggers the corresponding <wcs-fetch> element:

<button data-fetchtarget="user-fetch">Load Users</button>
<wcs-fetch id="user-fetch" url="/api/users"></wcs-fetch>

Event delegation is used — works with dynamically added elements. The closest() API handles nested elements (e.g., icon inside a button).

A matched click calls event.preventDefault() before triggering the fetch, so the element's default action is suppressed. This is intentional for the common case of firing a request without navigating. Avoid putting data-fetchtarget on an element whose default action you also want (e.g. a real <a href> link or a form-submit button) — the navigation/submit will be cancelled. Use a plain <button type="button">.

If the target id does not match any element, or the matched element is not a <wcs-fetch>, the click is silently ignored.

This is a convenience feature. In wcstack applications, state-driven triggering via trigger is usually the primary pattern.

Elements

<wcs-fetch>

| Attribute | Type | Default | Description | |-----------|------|---------|-------------| | url | string | — | Request URL | | method | string | GET | HTTP method | | target | string | — | DOM element id for HTML replace mode | | manual | boolean | false | Disable auto-fetch |

| Property | Type | Description | |----------|------|-------------| | value | any | Response data | | loading | boolean | true while request is in flight | | error | WcsFetchHttpError \| Error \| null | Error info | | status | number | HTTP status code | | body | any | Request body (resets to null after fetch()) | | trigger | boolean | Set to true to execute fetch | | manual | boolean | Explicit execution mode |

| Method | Description | |--------|-------------| | fetch() | Execute the HTTP request | | abort() | Cancel the in-flight request |

<wcs-fetch-header>

Defines a request header. Place it as a child of <wcs-fetch>.

| Attribute | Type | Description | |-----------|------|-------------| | name | string | Header name | | value | string | Header value |

<wcs-fetch-body>

Defines the request body. Place it as a child of <wcs-fetch>.

| Attribute | Type | Default | Description | |-----------|------|---------|-------------| | type | string | application/json | Content-Type |

The body content is taken from the element's text content.

Example:

<wcs-fetch url="/api/users" method="POST">
  <wcs-fetch-header name="Authorization" value="Bearer token123"></wcs-fetch-header>
  <wcs-fetch-header name="Accept" value="application/json"></wcs-fetch-header>
  <wcs-fetch-body type="application/json">
    {"name": "Tanaka", "email": "[email protected]"}
  </wcs-fetch-body>
</wcs-fetch>

wc-bindable-protocol

Both FetchCore and <wcs-fetch> declare wc-bindable-protocol compliance, making them interoperable with any framework or component that supports the protocol.

The declaration follows the full wc-bindable interface model — three independent surfaces:

  • properties — observable outputs that bind() subscribes to (value, loading, error, status, and the Shell's trigger)
  • inputs — the settable surface (url, method, …); declarative metadata that tooling, codegen, and remote proxying read
  • commands — invocable methods (fetch, abort); a binding system such as @wcstack/state can invoke them by name

Per the protocol, only properties is interpreted by core bind(); inputs / commands (and the attribute / async hints) are descriptive. They do not create implicit two-way data flow.

Core (FetchCore)

FetchCore declares the bindable async state that any runtime can subscribe to, plus its portable input/command surface:

static wcBindable = {
  protocol: "wc-bindable",
  version: 1,
  properties: [
    { name: "value",   event: "wcs-fetch:response",
      getter: (e) => e.detail.value },
    { name: "loading", event: "wcs-fetch:loading-changed" },
    { name: "error",   event: "wcs-fetch:error" },
    { name: "status",  event: "wcs-fetch:response",
      getter: (e) => e.detail.status },
  ],
  inputs: [
    { name: "url" },
    { name: "method" },
  ],
  commands: [
    { name: "fetch", async: true },
    { name: "abort" },
  ],
};

Headless consumers call core.fetch(url) directly — no trigger needed.

Shell (<wcs-fetch>)

The Shell extends the Core declaration with the trigger output and the DOM-driven input surface; commands (fetch / abort) are inherited unchanged:

static wcBindable = {
  ...FetchCore.wcBindable,
  properties: [
    ...FetchCore.wcBindable.properties,
    { name: "trigger", event: "wcs-fetch:trigger-changed" },
  ],
  inputs: [
    { name: "url" },
    { name: "method" },
    { name: "target" },
    { name: "manual" },
    { name: "body" },
    { name: "trigger" },
  ],
};

The Shell's inputs intentionally carry no attribute hint: each setter (url, method, target, manual) already reflects to its attribute, so a binding system that mirrors inputs[].attribute would set the attribute twice.

TypeScript Types

import type {
  WcsFetchHttpError, WcsFetchCoreValues, WcsFetchValues
} from "@wcstack/fetch";
// HTTP error (status >= 400)
interface WcsFetchHttpError {
  status: number;
  statusText: string;
  body: string;
}

// Core (headless) — 4 async state properties
// T defaults to unknown; pass a type argument for typed `value`
interface WcsFetchCoreValues<T = unknown> {
  value: T;
  loading: boolean;
  error: WcsFetchHttpError | Error | null;
  status: number;
}

// Shell (<wcs-fetch>) — extends Core with trigger
interface WcsFetchValues<T = unknown> extends WcsFetchCoreValues<T> {
  trigger: boolean;
}

Why this works well with @wcstack/state

@wcstack/state uses path strings as the only contract between UI and state. <wcs-fetch> fits this model naturally:

  • state computes url
  • <wcs-fetch> executes the request
  • async results return as value, loading, error, status
  • UI binds to those paths without writing fetch glue code

This makes async processing look like ordinary state updates.

Framework Integration

Since <wcs-fetch> exposes a CSBC wc-bindable-protocol contract, it works with any framework through thin adapters from @wc-bindable/*.

React

import { useWcBindable } from "@wc-bindable/react";
import type { WcsFetchValues } from "@wcstack/fetch";

interface User { id: number; name: string; }

function UserList() {
  const [ref, { value: users, loading, error }] =
    useWcBindable<HTMLElement, WcsFetchValues<User[]>>();

  return (
    <>
      <wcs-fetch ref={ref} url="/api/users" />
      {loading && <p>Loading...</p>}
      {error && <p>Error</p>}
      <ul>
        {users?.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </>
  );
}

Vue

<script setup lang="ts">
import { useWcBindable } from "@wc-bindable/vue";
import type { WcsFetchValues } from "@wcstack/fetch";

interface User { id: number; name: string; }

const { ref, values } = useWcBindable<HTMLElement, WcsFetchValues<User[]>>();
</script>

<template>
  <wcs-fetch :ref="ref" url="/api/users" />
  <p v-if="values.loading">Loading...</p>
  <p v-else-if="values.error">Error</p>
  <ul v-else>
    <li v-for="user in values.value" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

Svelte

<script>
import { wcBindable } from "@wc-bindable/svelte";

let users = $state(null);
let loading = $state(false);
</script>

<wcs-fetch url="/api/users"
  use:wcBindable={{ onUpdate: (name, v) => {
    if (name === "value") users = v;
    if (name === "loading") loading = v;
  }}} />

{#if loading}
  <p>Loading...</p>
{:else if users}
  <ul>
    {#each users as user (user.id)}
      <li>{user.name}</li>
    {/each}
  </ul>
{/if}

Solid

import { createWcBindable } from "@wc-bindable/solid";
import type { WcsFetchValues } from "@wcstack/fetch";

interface User { id: number; name: string; }

function UserList() {
  const [values, directive] = createWcBindable<WcsFetchValues<User[]>>();

  return (
    <>
      <wcs-fetch ref={directive} url="/api/users" />
      <Show when={!values.loading} fallback={<p>Loading...</p>}>
        <ul>
          <For each={values.value}>{(user) => <li>{user.name}</li>}</For>
        </ul>
      </Show>
    </>
  );
}

Vanilla — bind() directly

import { bind } from "@wc-bindable/core";

const fetchEl = document.querySelector("wcs-fetch");

bind(fetchEl, (name, value) => {
  console.log(`${name} changed:`, value);
});

Configuration

import { bootstrapFetch } from "@wcstack/fetch";

bootstrapFetch({
  autoTrigger: true,
  triggerAttribute: "data-fetchtarget",
  tagNames: {
    fetch: "wcs-fetch",
    fetchHeader: "wcs-fetch-header",
    fetchBody: "wcs-fetch-body",
  },
});

Design Notes

  • value, loading, error, and status are output state
  • url, body, and trigger are input / command surface
  • trigger is intentionally one-way: writing true executes, reset emits completion. Writing true while url is empty is silently ignored (no fetch, no event, flag stays false)
  • on an HTTP error (status >= 400), value is reset to null while status carries the error code — a value-only binding loses its previous value, so bind error to detect failures
  • on a network error (no HTTP response — DNS failure, offline, CORS, etc.), value is reset to null and status to 0; error holds the thrown Error. Like HTTP errors, a previous successful value/status does not linger
  • method="HEAD" skips response-body reading by spec (no body); value stays null and only status is surfaced
  • body is reset to null after each fetch() call — set it again before each submission
  • manual is useful when execution timing should be controlled explicitly
  • HTML replace mode is optional; the primary wcstack pattern is state-driven binding

License

MIT