最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to I keep two signals in sync (Service and Component) - Stack Overflow

programmeradmin1浏览0评论

I have this component, which is accepting a parameter as input.required ( say ID ).

  • I use this component in multiple places, with different IDs, I want to make sure the current input signal value get's updated on the service so that my resource always fetches the proper data.

  • I know I can achieve this by just directly assigning the service signal, but as mentioned earlier, I have multiple inputs, the component must have an id provided (input.required), but my resource lies at the service level (Shared by this component and it's children).

  • The service is scoped at the application level, so there is no need to add providers array, of course, If I want the service to be scoped at the component level I would do it.

Above are the requirements of my scenario, below if my minimal reproducible code:

Service:

@Injectable({
  providedIn: 'root',
})
export class SomeService {
  http = inject(HttpClient);
  serviceIdSignal: WritableSignal<number> = signal(0); // <- I want to sync this at the component level.

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

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

Child Component:

@Component({
  selector: 'app-child',
  template: `
        <div></div>
      `,
})
export class Child {
  someService = inject(SomeService);
  componentIdSignal: InputSignal<number> = input.required({
    alias: 'id',
  });
}

Root Component:

@Component({
  selector: 'app-root',
  imports: [Child, JsonPipe],
  template: `
    <app-child [id]="id()"/>
    <hr/>
    {{id()}}
    <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;
  someService = inject(SomeService);
  id = signal(1);
}

Stackblitz Demo

I have this component, which is accepting a parameter as input.required ( say ID ).

  • I use this component in multiple places, with different IDs, I want to make sure the current input signal value get's updated on the service so that my resource always fetches the proper data.

  • I know I can achieve this by just directly assigning the service signal, but as mentioned earlier, I have multiple inputs, the component must have an id provided (input.required), but my resource lies at the service level (Shared by this component and it's children).

  • The service is scoped at the application level, so there is no need to add providers array, of course, If I want the service to be scoped at the component level I would do it.

Above are the requirements of my scenario, below if my minimal reproducible code:

Service:

@Injectable({
  providedIn: 'root',
})
export class SomeService {
  http = inject(HttpClient);
  serviceIdSignal: WritableSignal<number> = signal(0); // <- I want to sync this at the component level.

  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());
    },
  });
}

Child Component:

@Component({
  selector: 'app-child',
  template: `
        <div></div>
      `,
})
export class Child {
  someService = inject(SomeService);
  componentIdSignal: InputSignal<number> = input.required({
    alias: 'id',
  });
}

Root Component:

@Component({
  selector: 'app-root',
  imports: [Child, JsonPipe],
  template: `
    <app-child [id]="id()"/>
    <hr/>
    {{id()}}
    <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;
  someService = inject(SomeService);
  id = signal(1);
}

Stackblitz Demo

Share Improve this question edited Feb 7 at 5:21 Naren Murali asked Feb 7 at 4:47 Naren MuraliNaren Murali 56.8k5 gold badges40 silver badges71 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

Syncing signals can we achieved using an effect, because it feels like updating another signal falls under the category of side effect and these should be done using an effect.

So we initialize an effect, which will do the sync to the service, this is very similar to a linkedSignal behavior (but we do not have access to the component from the service, so we use this method).

@Component({...})    
export class Child {
  someService = inject(SomeService);
  componentIdSignal: InputSignal<number> = input.required({
    alias: 'id',
  });

  constructor() {
    effect(() => {
      this.someService.serviceIdSignal.set(this.componentIdSignal());
    });
  }
}

To demonstrate this sync, we can create an incrementing ID at the root component level, this will trigger the resource APIs every 2 seconds.

@Component({...})
export class App {
  rs = ResourceStatus;
  someService = inject(SomeService);
  id = signal(1);

  ngOnInit() {
    setInterval(() => {
      this.id.update((prev) => ++prev);
    }, 2000);
  }
}

Full Code:

import {
  Component,
  signal,
  Injectable,
  WritableSignal,
  input,
  InputSignal,
  effect,
  inject,
  resource,
  ResourceStatus,
} 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);
  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({
  selector: 'app-child',
  template: `
    <div></div>
  `,
})
export class Child {
  someService = inject(SomeService);
  componentIdSignal: InputSignal<number> = input.required({
    alias: 'id',
  });

  constructor() {
    effect(() => {
      this.someService.serviceIdSignal.set(this.componentIdSignal());
    });
  }
}

@Component({
  selector: 'app-root',
  imports: [Child, JsonPipe],
  template: `
    <app-child [id]="id()"/>
    <hr/>
    {{id()}}
    <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;
  someService = inject(SomeService);
  id = signal(1);

  ngOnInit() {
    setInterval(() => {
      this.id.update((prev) => ++prev);
    }, 2000);
  }
}

bootstrapApplication(App, {
  providers: [provideHttpClient()],
});

Stackblitz Demo

发布评论

评论列表(0)

  1. 暂无评论