mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
d8ca560a15
commit
d0c9a6401a
8 changed files with 83 additions and 77 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
|
||||
// ...
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue