Search code examples
angulartypescriptfetch-apiangular-resourceangular-signals

Why do I need AbortSignal when using the resource API of angular. What is it's use?


I have been defining resource as follows:

resource: ResourceRef<any> = resource<ResourceRequest | undefined, any>({
  request: (): ResourceRequest | undefined => {
    return {
      id: this.id(),
    };
  },
  loader: ({ request: { id }, abortSignal }) => {
    return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`).then(
      (res: any) => res.json()
    );
  },
});

Seems sufficient to achieve what I want, but I see this pattern in blogs and youtube where they add the AbortSignal to the fetch request.

loader: ({ request: { id }, abortSignal }) => {
  return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
    signal: abortSignal,
  }).then((res: any) => res.json());
},

What is the necessity for this?

Below is my minimal reproducible code with working stackblitz.

TS:

id = signal(1);
rs = ResourceStatus;
http = inject(HttpClient);
resourceControl = signal(true);
resource: ResourceRef<any> = resource<ResourceRequest | undefined, any>({
  request: (): ResourceRequest | undefined => {
    return {
      id: this.id(),
    };
  },
  loader: ({ request: { id }, abortSignal }) => {
    return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`).then(
      (res: any) => res.json()
    );
  },
});

HTML:

<div>
  <div>
    Resource Request triggers:
  </div>
  <div>
    <input [(ngModel)]="id" type="number"/>
  </div>
</div>
<div>
  @if(![rs.Loading, rs.Reloading].includes(resource.status())) {
    {{resource.value() | json}}
  } @else{
    Loading...
  }
</div>

Stackblitz Demo


Solution

  • From the documentation:

    resource will cancel in-progress loads via the AbortSignal when destroyed or when a new request object becomes available, which could prematurely abort mutations.

    The syntax to configure the AbortController is as follows:

    resource: ResourceRef<any> = resource<ResourceRequest | undefined, any>({
      ...
      loader: ({ request: { id }, abortSignal }) => {
        return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
          signal: abortSignal
        }).then((res: any) => res.json();
      },
    });
    

    The signal property from RequestInit Object - MDN Docs

    An AbortSignal. If this option is set, the request can be canceled by calling abort() on the corresponding AbortController.

    So providing the AbortController will do two things:

    1. When a new request object arrives the previous still running requests are aborted (cancelled) - thus saving network resources.

    2. When the resource is itself destroyed the still running requests are aborted.


    In the below screenshots you can see the difference between using it and not using it:

    Without Abort Controller:

    without abort controller

    With Abort Controller:

    with abort controller

    For rxResource this is not a prerequisite, since it automatically performs this action, similar to the switchMap behavior of rxjs (does not use switchMap though).

    Full Code:

    import {
      Component,
      inject,
      ResourceRef,
      ResourceStatus,
      signal,
      resource,
    } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { rxResource } from '@angular/core/rxjs-interop';
    import { HttpClient, provideHttpClient } from '@angular/common/http';
    import { CommonModule } from '@angular/common';
    import { FormsModule } from '@angular/forms';
    
    export interface ResourceRequest {
      id: number;
    }
    
    @Component({
      selector: 'app-root',
      imports: [CommonModule, FormsModule],
      template: `
        <div>
          <div>
            Resource Request triggers:
          </div>
          <div>
            <input [(ngModel)]="id" type="number"/>
          </div>
        </div>
        <div>
          @if(![rs.Loading, rs.Reloading].includes(resource.status())) {
            {{resource.value() | json}}
          } @else{
            Loading...
          }
        </div>
      `,
    })
    export class App {
      id = signal(1);
      rs = ResourceStatus;
      http = inject(HttpClient);
      resourceControl = signal(true);
      resource: ResourceRef<any> = resource<ResourceRequest | undefined, any>({
        request: (): ResourceRequest | undefined => {
          return {
            id: this.id(),
          };
        },
        loader: ({ request: { id }, abortSignal }) => {
          return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
            signal: abortSignal,
          }).then((res: any) => res.json());
        },
      });
    }
    
    bootstrapApplication(App, {
      providers: [provideHttpClient()],
    });
    

    Stackblitz Demo