Async Data
@Resource is PraxisJS's decorator for binding async data to a component field — it tracks loading, error, and data state reactively, with automatic refetch, cancel, mutate, shared cache, and stale-while-revalidate.
Async Data
@Resource binds an async data source directly to a component field. It handles loading state, error handling, and automatic refetching when its reactive dependencies change — without any manual wiring.
npm install @praxisjs/core @praxisjs/decoratorspnpm add @praxisjs/core @praxisjs/decoratorsyarn add @praxisjs/core @praxisjs/decoratorsbun add @praxisjs/core @praxisjs/decoratorsBasic usage
import { Component, State, Resource } from '@praxisjs/decorators'
import type { ResourceInstance } from '@praxisjs/decorators'
import { StatefulComponent } from '@praxisjs/core'
@Component()
class UserProfile extends StatefulComponent {
@State() userId = 1
@Resource((self: UserProfile) =>
fetch(`/api/users/${self.userId}`).then(r => r.json() as Promise<User>)
)
user!: ResourceInstance<User>
render() {
return (
<div>
{() => this.user.pending() && <p>Loading...</p>}
{() => this.user.error() && <p>Error: {this.user.error()?.message}</p>}
{() => this.user.data() && <p>Hello, {this.user.data()!.name}</p>}
</div>
)
}
}When self.userId changes, the resource automatically cancels any pending request and starts a new one. The userId signal is read inside the fetcher, making it a tracked reactive dependency.
For fetchers with no component dependency, the short arrow form works without self:
@Resource(() => fetch('/api/config').then(r => r.json() as Promise<Config>))
config!: ResourceInstance<Config>Resource state
| Property | Type | Description |
|---|---|---|
.data() | T | null | The resolved value, or null before the first successful fetch |
.pending() | boolean | true while a fetch is in-flight |
.error() | Error | null | The last error, or null if no error |
.status() | 'idle' | 'pending' | 'success' | 'error' | Full status string |
All properties are reactive — read them inside {() => ...} in JSX to subscribe.
Options
@Resource(() => api.getUsers(), {
initialData: [], // value of .data() before the first fetch completes
immediate: false, // don't fetch automatically on init (default: true)
keepPreviousData: true, // keep old data visible while refetching
key: 'users', // cache key — enables SWR, deduplication, and invalidation
staleTime: 5000, // ms before cached data is considered stale (default: 0)
refetchOnFocus: true, // refetch when the tab/window regains focus
})
users!: ResourceInstance<User[]>keepPreviousData is useful for pagination: the current page stays visible while the next one loads.
Refetch, cancel, and mutate
// Manually trigger a new fetch
<button onClick={() => this.user.refetch()}>Refresh</button>
// Cancel an in-flight request (the promise is abandoned, not aborted)
<button onClick={() => this.user.cancel()}>Cancel</button>Optimistic updates
mutate() lets you update the UI immediately while the server request is in-flight:
async save(newName: string) {
// Update the UI now
this.user.mutate({ ...this.user.data()!, name: newName })
// Sync with the server, then confirm with a fresh fetch
await api.updateUser({ name: newName })
this.user.refetch()
}Shared cache and stale-while-revalidate
Setting a key enables three behaviors at once: a shared cache, deduplication, and stale-while-revalidate (SWR).
Stale-while-revalidate
When a resource with a key mounts and the cache already has data, it renders the cached (stale) value immediately and fetches fresh data in the background. The UI is never blank:
// First component to mount fetches and fills the cache
@Resource(() => api.getConfig(), { key: 'app-config' })
config!: ResourceInstance<AppConfig>
// Second component with the same key shows cached data instantly,
// then silently updates when the background fetch completes
@Resource(() => api.getConfig(), { key: 'app-config' })
config!: ResourceInstance<AppConfig>staleTime
staleTime (in milliseconds) controls how long cached data is considered fresh. While fresh, the initial fetch is skipped entirely:
@Resource(() => api.getConfig(), {
key: 'app-config',
staleTime: 60_000, // fresh for 60 s — no fetch if cache is younger
})
config!: ResourceInstance<AppConfig>staleTime | Behaviour |
|---|---|
0 (default) | Always stale — shows cached value, fetches in background (SWR) |
5000 | Fresh for 5 s — no fetch if the cache is newer than 5 s |
Infinity | Never refetch from cache — static until explicitly invalidated |
Deduplication
When multiple component instances share the same key and all try to fetch at the same time, only one network request is made. All instances attach to the same in-flight promise:
// Three instances on the same page — one fetch, shared result
@Resource(() => api.getCurrentUser(), { key: 'current-user' })
user!: ResourceInstance<User>key is designed for fetchers with no reactive dependencies (e.g. global config, current user). For reactive per-instance fetches where the fetcher reads self.someSignal, rely on PraxisJS's automatic dep tracking instead — key does not support dynamic values computed at runtime.
Invalidating by key
invalidateResource(key) clears the cache entry and triggers an immediate refetch on every active resource registered under that key — from anywhere in the app:
import { invalidateResource } from '@praxisjs/decorators'
// After a mutation, refresh all components showing the current user
async function updateProfile(data: ProfileUpdate) {
await api.updateProfile(data)
invalidateResource('current-user')
}@Component()
class ProfileCard extends StatefulComponent {
@Resource(() => api.getCurrentUser(), { key: 'current-user' })
user!: ResourceInstance<User>
// Automatically refetches when invalidateResource('current-user') is called
}Refetch on focus
When refetchOnFocus: true, the resource automatically refetches whenever the browser tab or window becomes visible again. This keeps data fresh after the user switches tabs:
@Resource(() => api.getNotifications(), {
key: 'notifications',
refetchOnFocus: true,
})
notifications!: ResourceInstance<Notification[]>refetchOnFocus uses document.visibilitychange and has no effect in non-browser environments (SSR, Node.js tests).
Reactive dependencies
Any signal read inside the fetcher is a tracked dependency. When it changes, the resource automatically cancels any pending request and starts a new one:
@State() page = 1
@State() search = ''
@Resource((self: SearchPage) =>
fetch(`/api/search?q=${self.search}&page=${self.page}`).then(r => r.json())
)
results!: ResourceInstance<SearchResult[]>Changing self.page or self.search triggers an immediate refetch because both signals are read inside the fetcher.
What's next?
- Decorators: State & Props — full
@Resourcereference with all options - Decorators: Watchers —
@Watchfor reacting to state changes in methods - Concurrency —
@Task,@Queue,@Poolfor complex async flows
Lifecycle Hooks
PraxisJS components expose four lifecycle hooks — onBeforeMount, onMount, onUnmount, and onError — available on both StatefulComponent and StatelessComponent.
Portal
Portal renders a component's JSX subtree into a different DOM node — escaping overflow, stacking context, and z-index constraints. Built into @praxisjs/runtime, no extra package needed.