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 badges1 Answer
Reset to default 0Syncing 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()],
});