@wcstack/fetch
v1.5.2
Published
Declarative fetch component for Web Components. Framework-agnostic async data fetching via wc-bindable-protocol.
Maintainers
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 HAWC architecture:
- Core (
FetchCore) handles HTTP, abort, and async state - Shell (
<wcs-fetch>) connects that state to the DOM - frameworks and binding systems consume it through wc-bindable-protocol
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:
- state computes
url <wcs-fetch>executes the request- async results return as
value,loading,error,status - UI binds to those paths with
data-wcs
This turns async communication into state transitions, not imperative UI code.
Install
npm install @wcstack/fetchQuick 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-fetch data-wcs="url: usersUrl; value: users"></wcs-fetch>
<ul>
<template data-wcs="for: users">
<li data-wcs="textContent: users.*.name"></li>
</template>
</ul>
</wcs-state>This is the default mode:
- connect
url - receive
value - optionally bind
loading,error, andstatus
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>
<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>
</wcs-state>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-fetch
url="/api/users"
manual
data-wcs="trigger: shouldRefresh; value: users; loading: listLoading">
</wcs-fetch>
<button data-wcs="onclick: reload">Refresh</button>
</wcs-state>trigger is a one-way command surface:
- writing
truestartsfetch() - it resets itself to
falseafter completion - the reset emits
wcs-fetch:trigger-changed
external write: false → true No event (triggers fetch)
auto-reset: true → false Dispatches wcs-fetch:trigger-changed4. 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>
<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>
</wcs-state>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 HAWC surface:
| Property | Type | Description |
|----------|------|-------------|
| value | any | Response data |
| loading | boolean | true while a request is in flight |
| error | WcsFetchHttpError \| Error \| null | HTTP or network error |
| status | number | HTTP status code |
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 HAWC architecture.
Core: FetchCore
FetchCore is a pure EventTarget class.
It contains:
- HTTP execution
- abort control
- async state transitions
wc-bindable-protocoldeclaration
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
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:
- it is connected to the DOM and
urlis set - the
urlchanges
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.
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.
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).
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.
Core (FetchCore)
FetchCore declares the bindable async state that any runtime can subscribe to:
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 },
],
};Headless consumers call core.fetch(url) directly — no trigger needed.
Shell (<wcs-fetch>)
The Shell extends the Core declaration with trigger so binding systems can execute fetch declaratively:
static wcBindable = {
...FetchCore.wcBindable,
properties: [
...FetchCore.wcBindable.properties,
{ name: "trigger", event: "wcs-fetch:trigger-changed" },
],
};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> is HAWC + wc-bindable-protocol, 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, andstatusare output stateurl,body, andtriggerare input / command surfacetriggeris intentionally one-way: writingtrueexecutes, reset emits completionbodyis reset tonullafter eachfetch()call — set it again before each submissionmanualis useful when execution timing should be controlled explicitly- HTML replace mode is optional; the primary wcstack pattern is state-driven binding
License
MIT
