Description
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