Skip to content

Resources & Async Data

Avenx-JS provides built-in reactive data fetching abstractions through the <resource> Single File Component (SFC) compiler tag and the runtime Resource class (lib/core/reactive/Resource.js). Resources automatically track reactive state dependencies using an internal AvenxWatcher, trigger automatic re-fetches when dependencies change, and expose a .read() method compatible with Suspense and Error Boundaries.


In Avenx Single File Components (.component.js), resources are declared at the top of component templates using <resource> tags. Avenx-JS supports two syntax formats:

Use block syntax to define inline asynchronous expressions or multi-line fetch logic:

<resource name="userData">
return fetch(`/api/users/${state.userId}`).then(res => res.json());
</resource>

[!NOTE] For single-line expressions in block syntax, if return is omitted and no trailing semicolon exists (e.g. fetch('/api/data').then(r => r.json())), the Avenx compiler automatically prepends return and appends a semicolon.

Use self-closing syntax to delegate resource fetching to a component action/method or an inline string expression:

<!-- Referencing an inline handler expression -->
<resource name="posts" handler="fetch('/api/posts').then(r => r.json())" />
<!-- Referencing a component action method -->
<resource name="profile" handler="this.fetchUserProfile" />

Configure automated background polling by specifying the pollInterval attribute in milliseconds on any <resource> tag:

<resource name="liveMetrics" pollInterval="5000">
return fetch('/api/metrics').then(res => res.json());
</resource>

Under the hood, every declared resource creates an instance of the Resource class (lib/core/reactive/Resource.js).

import { Resource } from 'avenx-core/reactive';
const resource = new Resource(name, handlerFn, componentContext, options);
Parameter Type Description
name string Unique string identifier for the resource.
handlerFn function(): any Function executing the asynchronous operation (e.g., returning a Promise).
componentContext object The containing component instance context (this).
options object (Optional) Configuration options object.
Option Type Default Description
pollInterval number 0 Interval in milliseconds for automated background data polling (disabled if 0).

Every Resource instance maintains the following public reactive properties:

interface ResourceInstance<T = any> {
/** Resource identifier string */
name: string;
/** Current lifecycle status */
status: 'idle' | 'pending' | 'resolved' | 'rejected';
/** Resolved data payload (undefined while pending/rejected) */
value: T | undefined;
/** Error instance or rejection reason (undefined if resolved/pending) */
error: Error | any;
/** Active Promise instance associated with the fetch */
promise: Promise<T> | null;
}
┌──────────┐
│ idle │
└────┬─────┘
│ (fetch initiated)
┌──────────┐
│ pending │
└────┬─────┘
┌─────┴────────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ resolved │ │ rejected │
└──────────┘ └──────────┘
Status Value Property (resource.value) Error Property (resource.error) Promise Property (resource.promise)
'idle' undefined undefined null
'pending' undefined undefined Active Promise<any>
'resolved' Resolved result T undefined Resolved Promise<T>
'rejected' undefined Thrown Error / Reason Rejected Promise<any>

The .read() method provides a Suspense- and Error Boundary-compatible getter:

const data = resource.read();
  • Pending: If status === 'pending', read() throws this.promise. Suspense boundaries catch this thrown Promise to display fallback loading UI.
  • Rejected: If status === 'rejected', read() throws this.error. Error Boundaries catch this thrown error to display fallback error UI.
  • Resolved: If status === 'resolved', read() returns this.value.

Cleans up the internal AvenxWatcher dependency tracker and clears any active background polling timer (pollTimer). This is invoked automatically when a component unmounts to prevent memory leaks and unnecessary background re-fetches:

resource.teardown();

Resource integrates directly with Avenx-JS reactivity (AvenxWatcher) and component update scheduler:

  1. Dependency Tracking: When a Resource is constructed, it wraps handlerFn in an AvenxWatcher. Any reactive state property accessed during handlerFn execution (such as state.userId or state.filter) is automatically registered as a dependency.
  2. Automatic Re-fetching: When any tracked state dependency mutates, AvenxWatcher triggers resource.fetch(newVal) automatically.
  3. Background Polling: When pollInterval is specified (> 0), Resource initializes pollTimer (setInterval). Every pollInterval milliseconds, it re-evaluates handlerFn to fetch fresh data in the background.
  4. Teardown & Cleanup: When the component unmounts, resource.teardown() automatically clears pollTimer via clearInterval alongside AvenxWatcher cleanup to prevent memory leaks.
  5. Component Re-rendering: When the async operation resolves or rejects, Resource marks componentContext.renderWatcher.dirty = true and invokes componentContext.update() to flush DOM updates.

The following Single File Component demonstrates background polling to periodically check real-time server health every 10 seconds:

src/components/server-status.component.js
export default {
template: `
<!-- Poll server status endpoint every 10 seconds -->
<resource name="serverStatus" pollInterval="10000">
return fetch('/api/health').then(r => r.json());
</resource>
<div class="status-widget">
<div data-ax-show="serverStatus.status === 'pending'" class="loading">
Checking server health...
</div>
<div data-ax-show="serverStatus.status === 'resolved'" class="status-badge">
Server Status: <strong>{{ serverStatus.value?.status }}</strong>
(Uptime: {{ serverStatus.value?.uptime }}s)
</div>
</div>
`,
};

The following Single File Component demonstrates a user directory search card using a <resource> tag, state dependency tracking (state.searchQuery), status checks, and error rendering:

src/components/user-search.component.js
export default {
state: {
searchQuery: 'alex',
},
actions: {
updateQuery(event) {
this.state.searchQuery = event.target.value;
},
},
template: `
<resource name="searchResults">
return fetch(\`/api/users/search?q=\${state.searchQuery}\`)
.then(res => {
if (!res.ok) throw new Error('Search request failed');
return res.json();
});
</resource>
<div class="user-search-card">
<input
type="text"
data-ax-bind="searchQuery"
@input="updateQuery"
placeholder="Search users..."
/>
<!-- Pending Loading State -->
<div data-ax-show="searchResults.status === 'pending'" class="spinner">
Loading user search results for "{{ state.searchQuery }}"...
</div>
<!-- Rejected Error State -->
<div data-ax-show="searchResults.status === 'rejected'" class="error-banner">
Error: {{ searchResults.error?.message }}
</div>
<!-- Resolved Data State -->
<ul data-ax-show="searchResults.status === 'resolved'" class="user-list">
<li data-ax-for="user in searchResults.value">
<strong>{{ user.name }}</strong> ({{ user.email }})
</li>
</ul>
</div>
`,
};

Rendering Large Datasets with <VirtualList>

Section titled “Rendering Large Datasets with <VirtualList>”

When fetching large arrays from an API (such as 1,000+ to 100,000+ items), rendering standard DOM elements for every array item with data-ax-for or <@for> can degrade browser rendering performance and increase memory usage.

To maintain 60 FPS scrolling and low memory footprint, pass the fetched resource array directly into the built-in <VirtualList> component inside <@suspense>:

<resource name="largeDataset">
return fetch('/api/large-dataset').then(r => r.json());
</resource>
<@suspense>
<@fallback>Loading large dataset...</@fallback>
<div style="height: 600px;">
<VirtualList :item-height="40" :items="largeDataset">
<template data-ax-as="item">
<div class="row">#{{ index + 1 }} — {{ item.name }}</div>
</template>
</VirtualList>
</div>
</@suspense>

[!TIP] <VirtualList> recycles DOM nodes as the user scrolls, rendering only the rows currently visible inside the viewport. Combined with <resource> and <@suspense>, this pattern provides instant loading indicators and smooth scrolling for large API responses.