Skip to content

Load async UI resources

Use a component resource when one element owns the request and its lifecycle. Use a store query when several elements share cached server data.

Public example API

The worked examples use https://cami-api.exe.xyz. It is a public, CORS-enabled mock service for Cami tutorials, not a production data store. It exposes /posts, /todos, /products, and /users; list routes support _limit, _start, and exact-field filters. Check availability at /health.

Load component-owned data

Call resource() during template() with a stable key:

import { html, ReactiveElement } from 'cami'

class UserBadge extends ReactiveElement {
  userId = '42'

  template() {
    const user = this.resource(
      `user:${this.userId}`,
      signal => fetch(`/api/users/${this.userId}`, { signal }).then(response => {
        if (!response.ok) throw new Error(`HTTP ${response.status}`)
        return response.json()
      }),
      { keepPrevious: true, race: 'latest' },
    )

    if (user.status === 'idle' || user.status === 'loading') {
      return html`<p>Loading…</p>`
    }
    if (user.status === 'error') {
      return html`<p role="alert">Could not load the user.</p>`
    }
    return html`<strong>${user.data.name}</strong>`
  }
}
import { html, ReactiveElement } from 'cami'

interface User {
  id: string
  name: string
}

class UserBadge extends ReactiveElement {
  userId: string = '42'

  template(): ReturnType<typeof html> {
    const user = this.resource<User>(
      `user:${this.userId}`,
      async (signal: AbortSignal): Promise<User> => {
        const response = await fetch(`/api/users/${this.userId}`, { signal })
        if (!response.ok) throw new Error(`HTTP ${response.status}`)
        return await response.json() as User
      },
      { keepPrevious: true, race: 'latest' },
    )

    if (user.status === 'idle' || user.status === 'loading') {
      return html`<p>Loading…</p>`
    }
    if (user.status === 'error') {
      return html`<p role="alert">Could not load the user.</p>`
    }
    return html`<strong>${user.data.name}</strong>`
  }
}

The state is a discriminated union with idle, loading, refreshing, success, and error statuses. Cami aborts active loaders when the element disconnects.

Load an image before displaying it

import { html, ReactiveElement, useImage } from 'cami'

class AvatarImage extends ReactiveElement {
  src: string = '/avatars/current-user.webp'

  template(): ReturnType<typeof html> {
    const image = useImage(this, this.src)
    if (image.status !== 'success') return html`<span>Loading avatar…</span>`
    return html`<img src=${image.data.src} width=${image.data.width} alt="Current user" />`
  }
}
import { html, ReactiveElement, useImage } from 'cami'

class AvatarImage extends ReactiveElement {
  src = '/avatars/current-user.webp'

  template() {
    const image = useImage(this, this.src)
    if (image.status !== 'success') return html`<span>Loading avatar…</span>`
    return html`<img src=${image.data.src} width=${image.data.width} alt="Current user" />`
  }
}

Use a shared store query

Define shared queries once, then execute them by name:

const UsersStore = store({ name: 'users', state: { users: [] } })

UsersStore.defineAction('replace', ({ state, payload }) => {
  state.users = payload
})

UsersStore.defineQuery('users:list', {
  queryKey: ['users'],
  queryFn: () => fetch('https://cami-api.exe.xyz/users').then(response => response.json()),
  onSuccess: ({ data, dispatch }) => dispatch('replace', data),
})

await UsersStore.query('users:list')
interface User {
  id: number
  name: string
  email: string
}

interface UsersState {
  users: User[]
}

const UsersStore = store<UsersState>({ name: 'users', state: { users: [] } })

UsersStore.defineAction('replace', ({ state, payload }) => {
  state.users = payload as User[]
})

UsersStore.defineQuery<void, User[]>('users:list', {
  queryKey: ['users'],
  queryFn: async (): Promise<User[]> => {
    const response = await fetch('https://cami-api.exe.xyz/users')
    return await response.json() as User[]
  },
  onSuccess: ({ data, dispatch }) => dispatch('replace', data),
})

await UsersStore.query<User[]>('users:list')

See the ObservableStore reference for cache, retry, invalidation, and mutation options.