PraxisJS

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, and mutate.

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.

Basic usage

import { Component, State, Resource } from '@praxisjs/decorators'
import { StatefulComponent } from '@praxisjs/core'

@Component()
class UserProfile extends StatefulComponent {
  @State() userId = 1

  @Resource(() => fetch(`/api/users/${this.userId}`).then(r => r.json()))
  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 this.userId changes, the resource automatically cancels any pending request and starts a new one. The userId signal is read inside the fetcher arrow function, making it a tracked dependency.

Storybook
Live demo — pagination + keepPreviousData

Resource state

PropertyTypeDescription
.data()T | nullThe resolved value, or null before the first successful fetch
.pending()booleantrue while a fetch is in-flight
.error()Error | nullThe 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
})
users!: ResourceInstance<User[]>

keepPreviousData is useful for pagination: the current page stays visible while the next one loads.

Storybook
Live demo — immediate: false

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()
}

Reactive dependencies

Any signal read inside the fetcher arrow function is a dependency. When it changes, the resource automatically cancels any pending request and starts a new one:

@State() page = 1
@State() search = ''

@Resource(() =>
  fetch(`/api/search?q=${this.search}&page=${this.page}`).then(r => r.json())
)
results!: ResourceInstance<SearchResult[]>

Changing this.page or this.search triggers an immediate refetch. Both are reactive dependencies because they're read inside the fetcher.


What's next?

On this page