Which @ngrx/* package(s) are relevant/related to the feature request?
signals
Information
Introduce a set of utilities for extending Angular Resource behavior with composable, reusable extensions.
Motivation
The current behavior of Angular Resource APIs is opinionated and leaves limited room for customization. Two common real-world requirements that are not configurable at the resource level:
- Value on loading — when a resource reloads,
value() resets to undefined until the new data arrives. There is no built-in way to preserve the previous value during a reload.
- Value on error — when a resource enters the error state,
value() throws. There is no built-in way to return a fallback or the previous value instead.
Angular recently introduced Resource Snapshots, which allow composing resources and can help with the "preserve value on loading" case. However, the error behavior remains unaddressable, and resourceFromSnapshot always returns the base Resource<T> type, meaning any additional APIs from more specific resource types (e.g. WritableResource) are lost in the process.
Overriding the default error behavior is also a prerequisite for introducing Resource integration for SignalStore. With the default behavior, once a resource enters the error state, any subsequent patchState call will throw because it reads value() internally — breaking the store.
Ideally, these behaviors would be configurable directly on the resource. Since they are not supported natively, this set of utilities offers what feels like the most ergonomic way to achieve this today: a thin extension layer that wraps any resource and patches only the behavior you want to change, while fully preserving the original resource type.
Prototype
Examples and source code of resource extensions are available here: https://github.com/markostanimirovic/extend-resource-prototype
Quick Start
import { extendResource, withPreviousValueOnLoading, withValueOnError } from 'extend-resource';
@Component({
/* ... */
})
export class TodoList {
readonly todosResource = extendResource({
resource: httpResource(() => `https://dummyjson.com/todos`),
extensions: [withPreviousValueOnLoading(), withValueOnError(undefined)],
});
}
API
extendResource
Applies extensions to a resource and returns it with its original type preserved.
const todosResource = extendResource({
resource: httpResource(() => `https://dummyjson.com/todos`),
extensions: [withPreviousValueOnLoading(), withValueOnError(undefined)],
});
provideResourceExtensions
Registers extensions for a given injector scope (application, route, component). Any resource wrapped with extendResource inside that scope will automatically have these extensions applied.
// main.ts
bootstrapApplication(App, {
providers: [provideResourceExtensions(withValueOnError(undefined))],
});
With global extensions in place, calling extendResource with just a resource is enough:
@Component({
/* ... */
})
export class TodoList {
// Global extensions are applied automatically
readonly todosResource = extendResource(httpResource(() => `/api/todos`));
}
Additional per-resource extensions are appended after the global ones:
@Component({
/* ... */
})
export class TodoList {
// Global extensions run first, then withPreviousValueOnLoading
readonly todosResource = extendResource({
resource: httpResource(() => `/api/todos`),
extensions: [withPreviousValueOnLoading()],
});
}
provideResourceExtensions is scope-aware and composes with parent injector extensions, so you can layer defaults at the app level and add more specific behavior at the route or component level without losing the parent configuration.
Extensions
Extensions are plain functions with the signature (resource: R) => void. They receive the resource and patch its behavior in place. The built-in extensions all follow the same pattern and can be freely combined.
withPreviousValueOnLoading()
Keeps the last resolved value while the resource is reloading, instead of resetting to undefined.
const todosResource = extendResource({
resource: httpResource(() => `/api/todos?page=${this.page()}`),
extensions: [withPreviousValueOnLoading()],
});
// value() returns the previous page's data while the next page is loading,
// rather than undefined.
withValueOnLoading(value)
Returns a specific fallback value while the resource is loading.
const todosResource = extendResource({
resource: httpResource(() => `/api/todos`),
extensions: [withValueOnLoading([])],
});
// value() returns [] on initial load and on every subsequent reload.
withPreviousValueOnError()
Returns the last successfully resolved value when the resource enters an error state, instead of throwing.
const todosResource = extendResource({
resource: httpResource(() => `/api/todos`),
extensions: [withPreviousValueOnError()],
});
// If the request fails, value() returns whatever was successfully loaded before,
// rather than throwing an error.
withValueOnError(value)
Returns a specific fallback value when the resource enters an error state, instead of throwing.
const todosResource = extendResource({
resource: httpResource(() => `/api/todos`),
extensions: [withValueOnError([])],
});
// If the request fails, value() returns [] instead of throwing.
I would be willing to submit a PR to fix this issue
Which @ngrx/* package(s) are relevant/related to the feature request?
signals
Information
Introduce a set of utilities for extending Angular
Resourcebehavior with composable, reusable extensions.Motivation
The current behavior of Angular Resource APIs is opinionated and leaves limited room for customization. Two common real-world requirements that are not configurable at the resource level:
value()resets toundefineduntil the new data arrives. There is no built-in way to preserve the previous value during a reload.value()throws. There is no built-in way to return a fallback or the previous value instead.Angular recently introduced Resource Snapshots, which allow composing resources and can help with the "preserve value on loading" case. However, the error behavior remains unaddressable, and
resourceFromSnapshotalways returns the baseResource<T>type, meaning any additional APIs from more specific resource types (e.g.WritableResource) are lost in the process.Overriding the default error behavior is also a prerequisite for introducing Resource integration for SignalStore. With the default behavior, once a resource enters the error state, any subsequent
patchStatecall will throw because it readsvalue()internally — breaking the store.Ideally, these behaviors would be configurable directly on the resource. Since they are not supported natively, this set of utilities offers what feels like the most ergonomic way to achieve this today: a thin extension layer that wraps any resource and patches only the behavior you want to change, while fully preserving the original resource type.
Prototype
Examples and source code of resource extensions are available here: https://github.com/markostanimirovic/extend-resource-prototype
Quick Start
API
extendResourceApplies extensions to a resource and returns it with its original type preserved.
provideResourceExtensionsRegisters extensions for a given injector scope (application, route, component). Any resource wrapped with
extendResourceinside that scope will automatically have these extensions applied.With global extensions in place, calling
extendResourcewith just a resource is enough:Additional per-resource extensions are appended after the global ones:
provideResourceExtensionsis scope-aware and composes with parent injector extensions, so you can layer defaults at the app level and add more specific behavior at the route or component level without losing the parent configuration.Extensions
Extensions are plain functions with the signature
(resource: R) => void. They receive the resource and patch its behavior in place. The built-in extensions all follow the same pattern and can be freely combined.withPreviousValueOnLoading()Keeps the last resolved value while the resource is reloading, instead of resetting to
undefined.withValueOnLoading(value)Returns a specific fallback value while the resource is loading.
withPreviousValueOnError()Returns the last successfully resolved value when the resource enters an error state, instead of throwing.
withValueOnError(value)Returns a specific fallback value when the resource enters an error state, instead of throwing.
I would be willing to submit a PR to fix this issue