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