Skip to content

Resources

Turn asynchronous fetches into signals, and show a fallback while they load.

A resource turns an asynchronous fetch into a signal, and View.Suspense shows a fallback while the resources below it load for the first time.

Info: A resource is a Signal.t of its loading state. Read it with Signal.get like any other signal.

Resources

Creating a Resource

Resource.make takes a function that returns a promise and starts fetching right away:

1let userId = Signal.make("1")
2
3let user = Resource.make(async () => {
4 let id = Signal.get(userId) // a dependency: read before the first await
5 let response = await fetch(`/api/users/${id}`)
6 await response->json
7})

The fetch runs in an effect. The signals it reads before its first await are its dependencies: change userId and the resource fetches again. Reads after an await are not tracked.

Reading the State

A resource holds one of four states:

  • Pending: fetching with no value yet, on the first fetch or after a failure.
  • Ready(value): the latest fetch settled with value.
  • Refreshing(value): fetching again, with the previous value kept.
  • Failed(exn): the latest fetch threw or rejected.
1module UserName = {
2 @xote.component
3 let make = () =>
4 <p>
5 {switch Signal.get(user) {
6 | Pending => "Loading..."
7 | Ready(user) | Refreshing(user) => user.name
8 | Failed(_) => "Could not load the user"
9 }}
10 </p>
11}

Resource.value returns the value a state carries, if any, and Resource.isLoading tells whether a fetch is in flight.

Refetching and Mutating

Resource.refetch fetches again with the same dependencies. Resource.mutate sets the value directly, which is useful for optimistic updates, and drops any fetch in flight.

1<button onClick={_ => Resource.refetch(user)}> {"Reload"} </button>

Only the latest fetch can settle the state. If the user switches from one id to another and the first response arrives last, it is ignored.

Errors

A failed fetch is a state like any other. To send it to a View.ErrorBoundary instead, throw it where the state is read:

1switch Signal.get(user) {
2| Failed(exn) => throw(exn)
3| state => ...
4}

Suspense

Showing a Fallback

Wrap the components that create resources in View.Suspense:

1<View.Suspense fallback={<p> {"Loading..."} </p>}>
2 <Profile id />
3 <Posts id />
4</View.Suspense>

The fallback shows until every resource created below the suspense has loaded once. The children render straight away, hidden, so their fetches start at once and their state survives the wait. Their refs and onMount callbacks are held back until they are shown.

Rules

  • A resource suspends the nearest Suspense above the component that created it, wherever it is read.
  • A resource counts once. A later fetch shows as Refreshing and never brings the fallback back.
  • Suspenses nest. The nearest one waits, and an inner subtree that is ready stays hidden while an outer suspense still waits.
  • Removing a component releases its resources: a fetch in flight is dropped, and a suspense waiting for it stops waiting.

Server Rendering

During a server render (SSR.renderToString) nothing is awaited: no fetch starts, and Suspense renders its fallback. A resource created outside a render, at module level in a server process, is not covered and does fetch; create resources in components. Hydration then renders the children on the client, where the resources fetch.

Next Steps