RFC(`@ngrx/signals`): Add `withResource`
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
- [x] Yes
- [ ] No
Hi Rainer and the team. Amazing work as usual. May I ask why did you chose this approach
const userStore = new UserStore();
userStore.listValue(); // User[]
userStore.listStatus(); // 'resolved' | 'loading' | 'error'
userStore.detailError(); // unknown | Error
userStore.detailHasValue(); // boolean
patchState(userStore, { listValue: [] });
instead of something like this?
const userStore = new UserStore();
userStore.list.value(); // User[]
userStore.list.status(); // 'resolved' | 'loading' | 'error'
userStore.detail.error(); // unknown | Error
userStore.detail.hasValue(); // boolean
patchState(userStore, { list: { value: [] } });
In my opinion, the second version has an obvious advantage because the nomenclature is closer to the one that Angular maintains in its Resource API.
Hello Daniel 👋
The first two iterations were actually quite close to your version.
The main issue is with patchState — it can only be applied to the state itself. That would require the entire resource to be part of the state, meaning it must be of type WritableSignal. But that's not the case for ResourceRef, where only value is a WritableSignal, while the rest are either methods or read-only Signal properties.
That’s why we decided to go with the current approach and introduced mapToResource as a helper function.
This looks fantastic! Excited to see this implemented once Resource is out of developer preview.
Is there a plan for extending withResource to support Entities? Would save a lot of headaches where I’d like to use an httpResource but cannot because my SignalStore is using entities. If I were to try and us a resource, I’m forced into a situation where I have to keep the Resource’s value in sync with my store / entities, so I end up dropping httpResource and run my API calls through an rxMethod. Of course that works, but I miss out on the ergonomics of native isLoading and the like.
@Cjameek, well this whole RFC is up for discussion. How would you see the integration to withEntities?
Hi 👋🏻
Based on the discussion about the Resource API, the limitation of patchState seems to be a recurring blocker to achieving more natural, nested APIs.
Thus, my suggestion is to directly address this limitation by enhancing patchState itself. What if it could accept a configuration object to target specific slices?
patchState({ store, slice: "parent" }, { child })
// This would patch store.parent.child
This single improvement would not only enable the more intuitive API we've been discussing but would also be a game-changer for other valuable features, like withKeyed, that are facing the same problem
Pushed even further, it could be used to bring harmony to the entire API landscape.
By allowing slices, this solution could create a unified approach across features like withKeyed (another great proposal to solving naming collisions) and even withEntities (instead of using collections), making the entire NgRx Toolkit more consistent and powerful.
@LcsGa, I don't really see that as a blocker. It is a design decision where some things work well and others not so.
About your suggestion. I've been thinkg about that multiple times, but didn't really know which approach is best in terms of DX. Angular's Signal Forms will bring a Signal Type which can be re-used here as well. So I would really like to wait until that one is out.
@Cjameek, well this whole RFC is up for discussion. How would you see the integration to withEntities?
Apologies for the late reply. That is a great question. Truthfully, I am unsure of the best path. I've spent time researching some kind of entityResource wrapper around httpResource that has mutatable state akin to Tan Stack Query's mutation query. Hypothetically, the Entity Adapter's entity updaters would be accessible via the entityResource:
entityResource.addEntity(myAddition)
entityResource.updateEntity({ id: 1, changes: mychanges })
But this hasn't worked out for multiple reasons. So at this point I'm unsure of an API that would work. It's also possible that the Resource API is simply not a good fit for Entity management, but there are better developers who could probably figure out something cool.