refactor(core): rename resource's request to params (#60919)

As decided in the resource RFC, this commit renames the `request` option of
a resource to `params`, including the subsequent argument passed to the
loader. It also corrects the type in the process to properly allow narrowing
of the `undefined` value.

Fixes #58871

PR Close #60919
This commit is contained in:
Alex Rickabaugh 2025-04-18 14:24:13 -07:00 committed by Miles Malerba
parent d8ca560a15
commit d0c9a6401a
8 changed files with 83 additions and 77 deletions

View file

@ -55,8 +55,8 @@ export class Search {
private readonly client = inject(ALGOLIA_CLIENT);
searchResults = resource({
request: () => this.searchQuery() || undefined, // coerces empty string to undefined
loader: async ({request: query, abortSignal}) => {
params: () => this.searchQuery() || undefined, // coerces empty string to undefined
loader: async ({params: query, abortSignal}) => {
// Until we have a better alternative we debounce by awaiting for a short delay.
await wait(SEARCH_DELAY, abortSignal);

View file

@ -14,24 +14,24 @@ import {resource, Signal} from '@angular/core';
const userId: Signal<string> = getUserId();
const userResource = resource({
// Define a reactive request computation.
// The request value recomputes whenever any read signals change.
request: () => ({id: userId()}),
// Define a reactive computation.
// The params value recomputes whenever any read signals change.
params: () => ({id: userId()}),
// Define an async loader that retrieves data.
// The resource calls this function every time the `request` value changes.
loader: ({request}) => fetchUser(request),
// The resource calls this function every time the `params` value changes.
loader: ({params}) => fetchUser(params),
});
// Create a computed signal based on the result of the resource's loader function.
const firstName = computed(() => userResource.value().firstName);
```
The `resource` function accepts a `ResourceOptions` object with two main properties: `request` and `loader`.
The `resource` function accepts a `ResourceOptions` object with two main properties: `params` and `loader`.
The `request` property defines a reactive computation that produce a request value. Whenever signals read in this computation change, the resource produces a new request value, similar to `computed`.
The `params` property defines a reactive computation that produces a parameter value. Whenever signals read in this computation change, the resource produces a new parameter value, similar to `computed`.
The `loader` property defines a `ResourceLoader`— an async function that retrieves some state. The resource calls the loader every time the `request` computation produces a new value, passing that value to the loader. See [Resource loaders](#resource-loaders) below for more details.
The `loader` property defines a `ResourceLoader`— an async function that retrieves some state. The resource calls the loader every time the `params` computation produces a new value, passing that value to the loader. See [Resource loaders](#resource-loaders) below for more details.
`Resource` has a `value` signal that contains the results of the loader.
@ -39,19 +39,19 @@ The `loader` property defines a `ResourceLoader`— an async function that retri
When creating a resource, you specify a `ResourceLoader`. This loader is an async function that accepts a single parameter— a `ResourceLoaderParams` object— and returns a value.
The `ResourceLoaderParams` object contains three properties: `request`, `previous`, and `abortSignal`.
The `ResourceLoaderParams` object contains three properties: `params`, `previous`, and `abortSignal`.
| Property | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `request` | The value of the resource's `request` computation. |
| `params` | The value of the resource's `params` computation. |
| `previous` | An object with a `status` property, containing the previous `ResourceStatus`. |
| `abortSignal` | An [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). See [Aborting requests](#aborting-requests) below for details. |
If the `request` computation returns `undefined`, the loader function does not run and the resource status becomes `'idle'`.
If the `params` computation returns `undefined`, the loader function does not run and the resource status becomes `'idle'`.
### Aborting requests
A resource aborts an outstanding request if the `request` computation changes while the resource is loading.
A resource aborts an outstanding loading operation if the `params` computation changes while the resource is loading.
You can use the `abortSignal` in `ResourceLoaderParams` to respond to aborted requests. For example, the native `fetch` function accepts an `AbortSignal`:
@ -59,7 +59,7 @@ You can use the `abortSignal` in `ResourceLoaderParams` to respond to aborted re
const userId: Signal<string> = getUserId();
const userResource = resource({
request: () => ({id: userId()}),
params: () => ({id: userId()}),
loader: ({request, abortSignal}): Promise<User> => {
// fetch cancels any outstanding HTTP requests when the given `AbortSignal`
// indicates that the request has been aborted.
@ -78,8 +78,8 @@ You can programmatically trigger a resource's `loader` by calling the `reload` m
const userId: Signal<string> = getUserId();
const userResource = resource({
request: () => ({id: userId()}),
loader: ({request}) => fetchUser(request),
params: () => ({id: userId()}),
loader: ({params}) => fetchUser(params),
});
// ...

View file

@ -179,7 +179,7 @@ export interface BaseResourceOptions<T, R> {
defaultValue?: NoInfer<T>;
equal?: ValueEqualityFn<T>;
injector?: Injector;
request?: () => R;
params?: () => R;
}
// @public
@ -1620,11 +1620,11 @@ export interface ResourceLoaderParams<R> {
// (undocumented)
abortSignal: AbortSignal;
// (undocumented)
params: NoInfer<Exclude<R, undefined>>;
// (undocumented)
previous: {
status: ResourceStatus;
};
// (undocumented)
request: Exclude<NoInfer<R>, undefined>;
}
// @public (undocumented)

View file

@ -315,7 +315,7 @@ class HttpResourceImpl<T>
) {
super(
request,
({request, abortSignal}) => {
({params: request, abortSignal}) => {
let sub: Subscription;
// Track the abort listener so it can be removed if the Observable completes (as a memory

View file

@ -30,8 +30,8 @@ describe('rxResource()', () => {
let unsub = false;
let lastSeenRequest: number = 0;
rxResource({
request,
loader: ({request}) => {
params: request,
loader: ({params: request}) => {
lastSeenRequest = request;
return new Observable((sub) => {
if (request === 2) {

View file

@ -128,7 +128,7 @@ export interface ResourceRef<T> extends WritableResource<T> {
* @experimental
*/
export interface ResourceLoaderParams<R> {
request: Exclude<NoInfer<R>, undefined>;
params: NoInfer<Exclude<R, undefined>>;
abortSignal: AbortSignal;
previous: {
status: ResourceStatus;
@ -163,7 +163,7 @@ export interface BaseResourceOptions<T, R> {
*
* If a request function isn't provided, the loader won't rerun unless the resource is reloaded.
*/
request?: () => R;
params?: () => R;
/**
* The value which will be returned from the resource when a server value is unavailable, such as

View file

@ -20,6 +20,7 @@ import {
ResourceStreamingLoader,
StreamingResourceOptions,
ResourceStreamItem,
ResourceLoaderParams,
} from './api';
import {ValueEqualityFn} from '../../primitives/signals';
@ -58,9 +59,12 @@ export function resource<T, R>(
export function resource<T, R>(options: ResourceOptions<T, R>): ResourceRef<T | undefined>;
export function resource<T, R>(options: ResourceOptions<T, R>): ResourceRef<T | undefined> {
options?.injector || assertInInjectionContext(resource);
const request = (options.request ?? (() => null)) as () => R;
const oldNameForParams = (
options as ResourceOptions<T, R> & {request: ResourceOptions<T, R>['params']}
).request;
const params = (options.params ?? oldNameForParams ?? (() => null)) as () => R;
return new ResourceImpl<T | undefined, R>(
request,
params,
getLoader(options),
options.defaultValue,
options.equal ? wrapEqualityFn(options.equal) : undefined,
@ -308,12 +312,14 @@ export class ResourceImpl<T, R> extends BaseWritableResource<T> implements Resou
// which side of the `await` they are.
const stream = await untracked(() => {
return this.loaderFn({
params: extRequest.request as Exclude<R, undefined>,
// TODO(alxhub): cleanup after g3 removal of `request` alias.
request: extRequest.request as Exclude<R, undefined>,
abortSignal,
previous: {
status: previousStatus,
},
});
} as ResourceLoaderParams<R>);
});
// If this request has been aborted, or the current request no longer

View file

@ -78,8 +78,8 @@ describe('resource', () => {
const counter = signal(0);
const backend = new MockEchoBackend();
const echoResource = resource({
request: () => ({counter: counter()}),
loader: (params) => backend.fetch(params.request),
params: () => ({counter: counter()}),
loader: (params) => backend.fetch(params.params),
injector: TestBed.inject(Injector),
});
@ -130,8 +130,8 @@ describe('resource', () => {
const backend = new MockEchoBackend();
const requestParam = {};
const echoResource = resource({
request: () => requestParam,
loader: (params) => backend.fetch(params.request),
params: () => requestParam,
loader: (params) => backend.fetch(params.params),
injector: TestBed.inject(Injector),
});
@ -149,9 +149,9 @@ describe('resource', () => {
const backend = new MockEchoBackend();
const counter = signal(0);
const echoResource = resource({
request: () => ({counter: counter()}),
params: () => ({counter: counter()}),
loader: (params) => {
if (params.request.counter % 2 === 0) {
if (params.params.counter % 2 === 0) {
return Promise.resolve('ok');
} else {
throw new Error('KO');
@ -186,10 +186,10 @@ describe('resource', () => {
const request = signal(0);
let resolve: Array<() => void> = [];
const res = resource({
request,
loader: async ({request}) => {
params: request,
loader: async ({params}) => {
const p = Promise.withResolvers<number>();
resolve.push(() => p.resolve(request));
resolve.push(() => p.resolve(params));
return p.promise;
},
injector: TestBed.inject(Injector),
@ -228,9 +228,9 @@ describe('resource', () => {
const DEFAULT: string[] = [];
const request = signal(0);
const res = resource({
request,
loader: async ({request}) => {
if (request === 2) {
params: request,
loader: async ({params}) => {
if (params === 2) {
throw new Error('err');
}
return ['data'];
@ -256,8 +256,8 @@ describe('resource', () => {
const counter = signal(0);
const backend = new MockEchoBackend();
const echoResource = resource({
request: () => (counter() > 5 ? {counter: counter()} : undefined),
loader: (params) => backend.fetch(params.request),
params: () => (counter() > 5 ? {counter: counter()} : undefined),
loader: (params) => backend.fetch(params.params),
injector: TestBed.inject(Injector),
});
@ -275,12 +275,12 @@ describe('resource', () => {
const backend = new MockEchoBackend<{counter: number}>();
const aborted: {counter: number}[] = [];
const echoResource = resource<{counter: number}, {counter: number}>({
request: () => ({counter: counter()}),
loader: ({request, abortSignal}) => {
abortSignal.addEventListener('abort', () => backend.abort(request));
return backend.fetch(request).catch((reason) => {
params: () => ({counter: counter()}),
loader: ({params, abortSignal}) => {
abortSignal.addEventListener('abort', () => backend.abort(params));
return backend.fetch(params).catch((reason) => {
if (reason === 'aborted') {
aborted.push(request);
aborted.push(params);
}
throw new Error(reason);
});
@ -309,12 +309,12 @@ describe('resource', () => {
const aborted: {counter: number}[] = [];
const injector = createEnvironmentInjector([], TestBed.inject(EnvironmentInjector));
const echoResource = resource<{counter: number}, {counter: number}>({
request: () => ({counter: counter()}),
loader: ({request, abortSignal}) => {
abortSignal.addEventListener('abort', () => backend.abort(request));
return backend.fetch(request).catch((reason) => {
params: () => ({counter: counter()}),
loader: ({params, abortSignal}) => {
abortSignal.addEventListener('abort', () => backend.abort(params));
return backend.fetch(params).catch((reason) => {
if (reason === 'aborted') {
aborted.push(request);
aborted.push(params);
}
throw new Error(reason);
});
@ -341,12 +341,12 @@ describe('resource', () => {
const aborted: {counter: number}[] = [];
const injector = createEnvironmentInjector([], TestBed.inject(EnvironmentInjector));
const echoResource = resource<{counter: number}, {counter: number}>({
request: () => ({counter: counter()}),
loader: ({request, abortSignal}) => {
abortSignal.addEventListener('abort', () => backend.abort(request));
return backend.fetch(request).catch((reason) => {
params: () => ({counter: counter()}),
loader: ({params, abortSignal}) => {
abortSignal.addEventListener('abort', () => backend.abort(params));
return backend.fetch(params).catch((reason) => {
if (reason === 'aborted') {
aborted.push(request);
aborted.push(params);
}
throw new Error(reason);
});
@ -372,11 +372,11 @@ describe('resource', () => {
const unrelated = signal('a');
const backend = new MockResponseCountingBackend();
const res = resource<string, number>({
request: () => 0,
params: () => 0,
loader: (params) => {
// read reactive state and assure it is _not_ tracked
unrelated();
return backend.fetch(params.request);
return backend.fetch(params.params);
},
injector: TestBed.inject(Injector),
});
@ -398,8 +398,8 @@ describe('resource', () => {
const counter = signal(0);
const backend = new MockEchoBackend();
const echoResource = resource({
request: () => ({counter: counter()}),
loader: (params) => backend.fetch(params.request),
params: () => ({counter: counter()}),
loader: (params) => backend.fetch(params.params),
injector: TestBed.inject(Injector),
});
@ -435,8 +435,8 @@ describe('resource', () => {
it('should allow re-fetching data', async () => {
const backend = new MockResponseCountingBackend();
const res = resource<string, number>({
request: () => 0,
loader: (params) => backend.fetch(params.request),
params: () => 0,
loader: (params) => backend.fetch(params.params),
injector: TestBed.inject(Injector),
});
@ -504,8 +504,8 @@ describe('resource', () => {
const request = signal<number | undefined>(undefined);
const backend = new MockEchoBackend();
const echoResource = resource({
request,
loader: (params) => backend.fetch(params.request),
params: request,
loader: (params) => backend.fetch(params.params),
injector: TestBed.inject(Injector),
});
// Idle to start.
@ -532,8 +532,8 @@ describe('resource', () => {
const request = signal<number | undefined>(1);
const backend = new MockEchoBackend();
const echoResource = resource({
request,
loader: (params) => backend.fetch(params.request),
params: request,
loader: (params) => backend.fetch(params.params),
injector: TestBed.inject(Injector),
});
const appRef = TestBed.inject(ApplicationRef);
@ -563,8 +563,8 @@ describe('resource', () => {
const request = signal<number | undefined>(1);
const backend = new MockEchoBackend();
const echoResource = resource({
request,
loader: (params) => backend.fetch(params.request),
params: request,
loader: (params) => backend.fetch(params.params),
injector: TestBed.inject(Injector),
});
const appRef = TestBed.inject(ApplicationRef);
@ -642,9 +642,9 @@ describe('resource', () => {
const stream = signal<{value: number} | {error: unknown}>({value: 0});
const request = signal(1);
const res = resource({
request,
stream: async ({request}) => {
if (request === 1) {
params: request,
stream: async ({params}) => {
if (params === 1) {
return stream;
} else {
return signal({value: 0});
@ -672,12 +672,12 @@ describe('resource', () => {
const backend = new MockEchoBackend<{counter: number} | null>();
const aborted: ({counter: number} | null)[] = [];
const echoResource = resource<{counter: number} | null, {counter: number} | null>({
request: () => ({counter: counter()}),
loader: ({request, abortSignal}) => {
abortSignal.addEventListener('abort', () => backend.abort(request));
return backend.fetch(request).catch((reason) => {
params: () => ({counter: counter()}),
loader: ({params, abortSignal}) => {
abortSignal.addEventListener('abort', () => backend.abort(params));
return backend.fetch(params).catch((reason) => {
if (reason === 'aborted') {
aborted.push(request);
aborted.push(params);
}
throw new Error(reason);
});