Skip to content

RFC(@ngrx/signals): Add withResource #4833

Open
@rainerhahnekamp

Description

@rainerhahnekamp

Important: withResource is not planned for v20.0 release. It becomes valid once Resource leaves the experimental status.

Which @ngrx/* package(s) are relevant/related to the feature request?

signals

Information

This proposal introduces a new SignalStore feature: withResource. It enables integrating any ResourceRef into a SignalStore instance, allowing store authors to declaratively manage resource state, status, and control methods like reload alongside their local state and methods.

Motivation

It is currently difficult to integrate a ResourceRef into a SignalStore in a clean and structured way. While it is possible to attach a resource manually via withProps, this approach does not provide full integration: resource members like value, status, or reload are not naturally part of the store’s API, nor do they follow consistent naming or lifecycle handling.

Given the relative newness of SignalStore, a large portion of current users are early adopters. These developers are also actively experimenting with newer features like resource, httpResource, and rxResource. Right now, they are faced with a trade-off: either use SignalStore or take full advantage of resource APIs.

By providing deep integration through withResource, developers no longer have to choose.

Design

withResource accepts a callback that takes state signals, props, and computed signals as an input argument and returns a ResourceRef (Unnamed Resource) or a dictionary of ResourceRef (Named Resources).

Basic Usage (Unnamed Resource)

When a single resource is provided, its members (value, status, error, hasValue, etc.) are merged directly into the store instance. This makes the store itself conform to the Resource<T> interface.

The value is stored as part of the SignalStore’s state under the value key. This means:

  • It can be updated via patchState() like any other state property.
  • It is exposed as a DeepSignal.

The reload method is added as a private _reload() method to prevent external use. Only the SignalStore itself should be able to trigger reloads.

const UserStore = signalStore(
  withState({ userId: undefined as number | undefined }),
  withResource(({ userId }) =>
    httpResource<User>(() =>
      userId === undefined ? undefined : `/users/${userId}`
    )
  )
);

const userStore = new UserStore();
userStore.value();          // User | undefined
userStore.status();         // 'resolved' | 'loading' | 'error' | ...
userStore.error();          // unknown | Error
userStore.hasValue();       // boolean

patchState(userStore, { value: { id: 1, name: 'Test' } });

Named Resources (Composition)

To support multiple resources within a store, a Record<string, ResourceRef> can be passed to withResource. Each resource will be prefixed using its key.

All members (value, status, error, hasValue, etc.) are added to the store using the resource key as a prefix. Each value is stored in the state and exposed as a DeepSignal, just like in the unnamed case.

const UserStore = signalStore(
  withState({ userId: undefined as number | undefined }),
  withResource(({ userId }) => ({
    list: httpResource<User[]>(() => '/users', { defaultValue: [] }),
    detail: httpResource<User>(() =>
      userId === undefined ? undefined : `/users/${userId}`
    ),
  }))
);

const userStore = new UserStore();
userStore.listValue();        // User[]
userStore.listStatus();       // 'resolved' | 'loading' | 'error'
userStore.detailError();      // unknown | Error
userStore.detailHasValue();   // boolean

patchState(userStore, { listValue: [] });

Each named resource also receives a prefixed reload method, e.g. _listReload() or _detailReload(), which are intentionally private and intended for internal store logic only.

Interoperability with Resource<T>

Named resources don’t directly conform to the Resource<T> type, but they can be mapped back using the mapToResource() utility:

const detailResource = mapToResource(userStore, 'detail');
use(detailResource); // Resource<User | undefined>

This enables compatibility with existing APIs or utilities that expect a Resource<T>.

Describe any alternatives/workarounds you're currently using

No response

I would be willing to submit a PR to fix this issue

  • Yes
  • No

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions