Skip to main content
Solar’s useResource hook handles async data fetching with automatic request cancellation. Pass it a key — any serializable value — and a fetch function that receives an AbortSignal. When the key changes (for example, the user selects a different record), useResource aborts the previous request with AbortController.abort() and starts a new one immediately. You never write cancellation logic yourself, and you never worry about stale responses from an old request overwriting fresh data.

Basic usage

Define a component that accepts an ID prop, fetch the corresponding record, and render the three possible states: loading, error, and success.
import { createElement, defineComponent, useResource, registry } from './solar/index.js'

const UserCard = defineComponent({
  name: 'UserCard',
  props: {
    userId: { type: 'number', required: true },
  },
  render({ userId }) {
    const { data, loading, error } = useResource({
      key: userId,
      fetch: async (signal) => {
        const res = await fetch(`/api/users/${userId}`, { signal })
        if (!res.ok) throw new Error(`HTTP ${res.status}`)
        return res.json()
      },
    })

    if (loading) return createElement('p', {}, 'Loading...')
    if (error)   return createElement('p', {}, `Error: ${error.message}`)

    return createElement('div', { class: 'user-card' },
      createElement('h3', {}, data.name),
      createElement('p', {}, data.email),
    )
  },
})

registry.register(UserCard)
export default UserCard
On the first render, loading is true and data is null. Once the fetch resolves, Solar re-renders the component automatically and loading drops to false.

How re-fetching works

The key parameter controls when useResource starts a new request. Solar serializes the key with JSON.stringify and compares it to the previous render’s key. When they differ, it calls controller.abort() on the previous AbortController and creates a new one for the next fetch.
// Renders once with userId = 1 → fetches /api/users/1
// User navigates → userId changes to 2
// useResource aborts the /api/users/1 request (if still in flight)
// and immediately starts /api/users/2

const { data, loading, error } = useResource({
  key: userId,           // changing this triggers cancellation + re-fetch
  fetch: async (signal) => {
    const res = await fetch(`/api/users/${userId}`, { signal })
    return res.json()
  },
})
Pass any serializable value as the key — a number, string, or an object like { userId, page }. Use an object key when your fetch depends on multiple values that should each trigger a re-fetch independently.
// Re-fetches whenever either userId or page changes
const { data, loading, error } = useResource({
  key: { userId, page },
  fetch: async (signal) => {
    const res = await fetch(`/api/users/${userId}/posts?page=${page}`, { signal })
    return res.json()
  },
})

The return value

useResource returns an object with three fields. Destructure all three and handle each in your render output.
FieldTypeDescription
dataany | nullThe resolved value from your fetch function. null until the first successful fetch.
loadingbooleantrue while a request is in flight. Drops to false when the fetch resolves or rejects.
errorError | nullThe caught error if the fetch rejected, or null if the last fetch was successful.
Always check loading first. Both loading and error can be false/null simultaneously — that’s the success state where data contains the result.

Combining with useSubscription

Use useResource and useSubscription together in the same render function. Each hook manages its own state slot independently.
import { createElement, defineComponent, useState, useResource, useSubscription, registry } from './solar/index.js'

const UserCard = defineComponent({
  name: 'UserCard',
  props: {
    userId: { type: 'number', required: true },
  },
  render({ userId }) {
    const [width, setWidth] = useState(window.innerWidth)

    const { data, loading, error } = useResource({
      key: userId,
      fetch: async (signal) => {
        const res = await fetch(`/api/users/${userId}`, { signal })
        return res.json()
      },
    })

    useSubscription({
      source: window,
      event: 'resize',
      handler: () => setWidth(window.innerWidth),
    })

    if (loading) return createElement('p', {}, 'Loading...')
    if (error)   return createElement('p', {}, `Error: ${error.message}`)

    return createElement('p', {}, `${data.name} — viewport: ${width}px`)
  },
})

registry.register(UserCard)
useSubscription attaches the resize listener once and re-attaches only if source, event, or handler changes. It does not re-run on every render.
Always call hooks in the same order on every render. Solar uses positional hook slots — calling a hook conditionally (inside an if block) shifts all subsequent hook indices and corrupts state. Declare all hooks at the top of render(), before any conditional returns.

Stale data while loading

When the key changes and a new fetch starts, data retains the value from the previous successful fetch for the duration of the new request. loading is true, but data is not null.
const { data, loading, error } = useResource({ key: userId, fetch: ... })

// After first fetch:   loading = false, data = { name: 'Alice', ... }
// After userId changes: loading = true,  data = { name: 'Alice', ... }  ← stale, but present
// After second fetch:  loading = false, data = { name: 'Bob', ... }
Use this to avoid a blank or spinner-only state on re-fetches. Render the stale data with a visual loading indicator rather than replacing the whole view with “Loading…”.
return createElement('div', { class: loading ? 'user-card user-card--loading' : 'user-card' },
  data
    ? createElement('p', {}, data.name)
    : createElement('p', {}, 'Loading...'),
)
Always pass the signal argument to your fetch() call. If you ignore it, useResource cannot cancel the in-flight request when the key changes — the old response will still arrive and trigger a re-render with stale data. Every fetch function you pass to useResource must accept and forward the AbortSignal.
// ✅ Correct — signal is forwarded
fetch: async (signal) => {
  const res = await fetch('/api/data', { signal })
  return res.json()
}

// ❌ Wrong — cancellation is silently ignored
fetch: async (_signal) => {
  const res = await fetch('/api/data')
  return res.json()
}