htmlangulartypescriptangular-resourceangular-signals

Can resource or rxResource be initialized through function or activated through button click


I tried to initialize my signal through button click, but I am getting the error:

ERROR RuntimeError: NG0203: rxResource() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with runInInjectionContext. Find more at https://angular.dev/errors/NG0203at assertInInjectionContext (core.mjs:2479:11)at rxResource (rxjs-interop.mjs:326:21)at _App.getUsers (main.ts:36:21)at App_Template_button_click_1_listener (main.ts:25:22)at executeListenerWithErrorHandling (core.mjs:29249:12)at wrapListenerIn_markDirtyAndPreventDefault (core.mjs:29281:18)at HTMLButtonElement. (platform-browser.mjs:806:112)at _ZoneDelegate.invokeTask (zone.js:402:33)at core.mjs:6164:49at AsyncStackTaggingZoneSpec.onInvokeTask (core.mjs:6164:30)

Below is the minimal reproducible code:

HTML:

@if(rxResource) {
  @if(![rs.Loading, rs.Reloading].includes(rxResource.status())) {
    {{rxResource.value() | json}}
  } @else{
    Loading...
  }
}
@if(resource) {
  @if(![rs.Loading, rs.Reloading].includes(resource.status())) {
    {{resource.value() | json}}
  } @else{
    Loading...
  }
}
<button (click)="getUsers()">Get Users</button>
<button (click)="reset()">Reset</button>

TS:

rs = ResourceStatus;
http = inject(HttpClient);
resource!: ResourceRef<any>;
rxResource!: ResourceRef<any>;

getUsers() {
  this.rxResource = rxResource({
    loader: () => {
      return this.http
        .get(`https://jsonplaceholder.typicode.com/todos`)
        .pipe(map((data: any) => data.slice(0, 10)));
    },
  });
  this.resource = resource({
    loader: () => {
      return fetch(`https://jsonplaceholder.typicode.com/todos/`)
        .then((res: any) => res.json())
        .then((res: any) => {
          return res.slice(0, 10);
        });
    },
  });
}

Stackblitz Demo


Solution

  • All you need to do is the provide the Injector DI element to the resource or rxResource and it will work fine.

    You need to provide Injector in all places, except the below scenarios:

    1. Declaring at the root of the component class.
    2. Declaring inside the constructor.

    Because the injector is available inside and the resource API can access it directly.

    export class App {
      ...
      injector = inject(Injector); // <- getting injector from DI!
    
      getUsers() {
        this.rxResource = rxResource({
          loader: () => {
            return this.http
              .get(`https://jsonplaceholder.typicode.com/todos`)
              .pipe(map((data: any) => data.slice(0, 10)));
          },
          injector: this.injector, // <- provide injector here!
        });
        this.resource = resource({
          loader: ({ abortSignal }) => {
            return fetch(`https://jsonplaceholder.typicode.com/todos/`, {
              signal: abortSignal,
            })
              .then((res: any) => res.json())
              .then((res: any) => {
                return res.slice(0, 10);
              });
          },
          injector: this.injector, // <- provide injector here!
        });
      }
    

    Full Code:

    import {
      Component,
      inject,
      ResourceRef,
      ResourceStatus,
      Injector,
      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 { map } from 'rxjs';
    
    @Component({
      selector: 'app-root',
      imports: [CommonModule],
      template: `
        <div>
          @if(rxResource) {
            @if(![rs.Loading, rs.Reloading].includes(rxResource.status())) {
              {{rxResource.value() | json}}
            } @else{
              Loading...
            }
          }
        </div>
        <hr/>
        <div>
          @if(resource) {
            @if(![rs.Loading, rs.Reloading].includes(resource.status())) {
              {{resource.value() | json}}
            } @else{
              Loading...
            }
          }
        </div>
        <hr/>
        <div>
        <button (click)="getUsers()">Get Users</button>
        <button (click)="reset()">Reset</button>
        </div>
      `,
    })
    export class App {
      rs = ResourceStatus;
      http = inject(HttpClient);
      resource!: ResourceRef<any>;
      rxResource!: ResourceRef<any>;
      injector = inject(Injector);
    
      getUsers() {
        this.rxResource = rxResource({
          loader: () => {
            return this.http
              .get(`https://jsonplaceholder.typicode.com/todos`)
              .pipe(map((data: any) => data.slice(0, 10)));
          },
          injector: this.injector,
        });
        this.resource = resource({
          loader: ({ abortSignal }) => {
            return fetch(`https://jsonplaceholder.typicode.com/todos/`, {
              signal: abortSignal,
            })
              .then((res: any) => res.json())
              .then((res: any) => {
                return res.slice(0, 10);
              });
          },
          injector: this.injector,
        });
      }
    
      reset() {
        if (this.resource) {
          this.resource.destroy();
        }
        if (this.rxResource) {
          this.rxResource.destroy();
        }
      }
    }
    
    bootstrapApplication(App, {
      providers: [provideHttpClient()],
    });
    

    Stackblitz Demo