htmlangulartypescriptangular-resourceangular-signals

I have signals in the service, I want to expose these signals to components (without using getter)


I have this scenario, My service holds few signals (They are use by resource API's to reactively fetch data and also show at the UI level)

My requirement is that I want to expose these values to the User in the component HTML.

My challenges are:

  1. I generally use a getter and setter, to provide these properties from a service (Private - not accessible in HTML).

  2. I do not want to use a computed here because, there is no actual computation happening, I just need the service signal reference, this scenario definitely does not get classified as a derived state, it is just a reference to a signal from the service.

  3. I do not want to make my service public, I want to expose certain elements alone, I would keep the extra elements as private, but using service in HTML is not an option for me.

These are my requirements, below is my minimal reproducible code:

Service:

@Injectable({
  providedIn: 'root',
})
export class SomeService {
  http = inject(HttpClient);
  serviceIdSignal: WritableSignal<number> = signal(0);

  rxResource = rxResource({
    request: () => this.serviceIdSignal(),
    loader: ({ request: id }) => {
      return this.http.get(`https://jsonplaceholder.typicode.com/todos/${id}`);
    },
  });

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

Component:

@Component({
  selector: 'app-root',
  imports: [JsonPipe],
  template: `
    <hr/>
    {{someService.serviceIdSignal()}}
    <hr/>
    @if(someService.rxResource.status() === rs.Resolved) {
      {{someService.rxResource.value() | json}}
    } @else {
      Loading...
    }
    <hr/>
    @if(someService.rxResource.status() === rs.Resolved) {
      {{someService.resource.value() | json}}
    } @else {
      Loading...
    }
  `,
})
export class App {
  rs = ResourceStatus;
  private someService = inject(SomeService);
}

Below is the error I am getting:

✘ [ERROR] NG1: Property 'someService' is private and only accessible within class 'App'. [plugin angular-compiler]

src/main.ts:56:12:
  56 │           {{someService.resource.value() | json}}
     ╵             ~~~~~~~~~~~

Stackblitz Demo


Solution

  • The advantages (might be) of signals is that they are functions and not primitive values, by that I mean, like arrays and objects, signal elements (computed, resource, rxResource, linkedSignal) are stored as memory references.

    You can directly refer these memory references instead of using a computed (Which as you said in the question, the scenario does not qualify as a derived state, but a referral to the actual state).

    My point is, create properties that directly reference the signals from the service and make them public:

    export class App {
      rs = ResourceStatus;
      private someService = inject(SomeService);
      // Signals are function, so they are memory reference, you can refer to them directly
      // without using getter or computed, which is much simpler!
      id: WritableSignal<number> = this.someService.serviceIdSignal; // <- refer directly to the service signal
      rxResource: ResourceRef<any> = this.someService.rxResource; // <- refer directly to the service rxResource
      resource: ResourceRef<any> = this.someService.resource; // <- refer directly to the service resource
    }
    

    You can use these newly created properties as ordinary signal elements, by directly executing them.

    <hr/>
    {{id()}}
    <hr/>
    @if(rxResource.status() === rs.Resolved) {
      {{rxResource.value() | json}}
    } @else {
      Loading...
    }
    <hr/>
    @if(rxResource.status() === rs.Resolved) {
      {{resource.value() | json}}
    } @else {
      Loading...
    }
    

    Full Code:

    import {
      Component,
      signal,
      Injectable,
      WritableSignal,
      inject,
      resource,
      ResourceStatus,
      ResourceRef,
    } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { rxResource } from '@angular/core/rxjs-interop';
    import { HttpClient, provideHttpClient } from '@angular/common/http';
    import { JsonPipe } from '@angular/common';
    
    @Injectable({
      providedIn: 'root',
    })
    export class SomeService {
      http = inject(HttpClient);
      readonly serviceIdSignal: WritableSignal<number> = signal(1);
    
      readonly rxResource = rxResource({
        request: () => this.serviceIdSignal(),
        loader: ({ request: id }) => {
          return this.http.get(`https://jsonplaceholder.typicode.com/todos/${id}`);
        },
      });
    
      readonly resource = resource({
        request: () => this.serviceIdSignal(),
        loader: ({ request: id, abortSignal }) => {
          return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
            signal: abortSignal,
          }).then((r) => r.json());
        },
      });
    }
    
    @Component({
      selector: 'app-root',
      imports: [JsonPipe],
      template: `
        <hr/>
        {{id()}}
        <hr/>
        @if(rxResource.status() === rs.Resolved) {
          {{rxResource.value() | json}}
        } @else {
          Loading...
        }
        <hr/>
        @if(rxResource.status() === rs.Resolved) {
          {{resource.value() | json}}
        } @else {
          Loading...
        }
      `,
    })
    export class App {
      rs = ResourceStatus;
      private someService = inject(SomeService);
      // Signals are function, so they are memory reference, you can refer to them directly
      // without using getter or computed, which is much simpler!
      id: WritableSignal<number> = this.someService.serviceIdSignal; // <- refer directly to the service signal
      rxResource: ResourceRef<any> = this.someService.rxResource; // <- refer directly to the service rxResource
      resource: ResourceRef<any> = this.someService.resource; // <- refer directly to the service resource
    }
    
    bootstrapApplication(App, {
      providers: [provideHttpClient()],
    });
    

    Stackblitz Demo