Skip to content

RFC: resource snapshots and withResource #292

@michael-small

Description

@michael-small

How do we handle the new resource snapshot API in conjunction with withResource?

You can create new resources from snapshots using resourceFromSnapshots. This enables composition with signal APIs like computed and linkedSignal to transform resource behavior.

  • About snapshots
  • Snapshot composition limitations
  • Open question on how to integrate resource snapshots + withResource

Basic reference Stackblitz I made for type inference, nothing toolkit related itself: https://stackblitz.com/edit/stackblitz-starters-kohef1nl?file=src%2Fmain.ts

Resource snapshots: "enables composition with signal APIs ... to transform resource behavior"

21.2.0 introduced resource composition with snapshots.

  • A ResourceSnapshot is a structured representation of a resource's current state. Every resource has a snapshot property that provides a signal of its current state.
  • Each snapshot contains a status and either a value or an error.
  • You can create new resources from snapshots using resourceFromSnapshots. This enables composition with signal APIs like computed and linkedSignal to transform resource behavior.

Example from the docs:

import {linkedSignal, resourceFromSnapshots, Resource, ResourceSnapshot} from '@angular/core';

function withPreviousValue<T>(input: Resource<T>): Resource<T> {
  const derived = linkedSignal<ResourceSnapshot<T>, ResourceSnapshot<T>>({
    source: input.snapshot,
    computation: (snap, previous) => {
      if (snap.status === 'loading' && previous && previous.value.status !== 'error') {
        // When the input resource enters loading state, we keep the value
        // from its previous state, if any.
        return {status: 'loading' as const, value: previous.value.value};
      }
      // Otherwise we simply forward the state of the input resource.
      return snap;
    },
  });
  return resourceFromSnapshots(derived);
}

@Component({
  /*... */
})
export class AwesomeProfile {
  userId = input.required<number>();
  user = withPreviousValue(httpResource(() => `/user/${this.userId()}`));
  // When userId changes, user.value() keeps the old user data until the new one loads
}

This resource snapshot API opens up all sorts of possibilities, like this withPreviousValue which alleviates the need to have a helper linkedSignal manually made for every single resource to hold the previous state while loading.

The problem - limitations of snapshot composition

Snapshots are useful for accessing resource data like value/status/error/isLoading/hasValue which is derived from however you have configured the snapshot. However, imperative actions are not supported.

For reference of types (reference Stackblitz):

  userResource: HttpResourceRef<string | undefined> = httpResource<string>(
    () => `/user/${this.userId()}`
  );
  // `ResourceRef` for `resource` or `rxResource` 

  userResourceFromSnapshot: Resource<string | undefined> = withPreviousValue(
    httpResource<string>(() => `/user/${this.userId()}`)
  );

Resources as they have been used before snapshots are of type ResourceRef (for resource and rxResource) or HttpResourceRef, which you can call set, update, reload, or destroy on. And they also both have a snapshot property. And for HttpResourceRef, there is also access to headers/statusCode/progress as well.

These snapshots are of type Resource, so they only have value/status/error/isLoading/hasValue and the snapshot property. They cannot be set or updated or reloaded. Snapshots of httpResource do not have access to headers/statusCode/progress.

How do we harness resources snapshots and withResource?

My initial idea: add an optional argument that takes a Resource function derived from the respective helper function that returns resourceFromSnapshots(saidSnapshottedResource), like the withPreviousValue example.

// Example from the Angular docs: 
// "When the input resource enters loading state, we keep the value from its previous state, if any. 
// Otherwise we simply forward the state of the input resource."
function withPreviousValue<T>(input: Resource<T>): Resource<T> {
  const derived = linkedSignal<ResourceSnapshot<T>, ResourceSnapshot<T>>({
    // ...
  });
  return resourceFromSnapshots(derived);
}

export const UserStore = signalStore(
  withState({ userId: 1 }),
  // optional `snapshotFn` argument would be the same for 
  // single resources or multiple named resources
  withResource(
    (state) => httpResource(() => `/user/${state.userId}`),
    { snapshotFn: withPreviousValue }
  ),
);

This way we still have all resource functionality, including imperative actions and some of the state lost to these resource snapshots, but still otherwise retain the snapshot's state helping capabilities.

This is just my initial idea and I have not attempted such implementation yet, but that's what I am initially envisioning.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions