If you're looking to enhance the functionality of your service layer, I would recommend replacing the Signal with either a Subject or BehaviourSubject.
An RxJS Subject is a unique type of Observable that also acts as an Observer. This allows us to manually input data into it, making it convenient for notifying multiple listeners simultaneously.
In this scenario, I will opt for a BehaviourSubject, which is a specialized version of Subject that stores a default value and retains the last emitted value for future subscribers (ensuring that it always emits a value upon subscription).
Take a look at the sample service below:
@Injectable({
providedIn: 'root'
})
export class Service {
public notifier = new BehaviorSubject<string | undefined>(undefined);
public fetchData(data: string) {
// business logic here
}
}
The components requiring access to the Subject's value for fetching remote data can now subscribe to it for notifications. The parent component can utilize the .next function to update the value:
@Component({
selector: 'app-parent',
template: ``,
standalone: true,
})
export class ParentComponent implements OnInit {
constructor(
private service: Service
) {
}
ngOnInit() {
this.service.notifier.next("NewValue");
}
}
@Component({
selector: 'app-child-a',
template: ``,
standalone: true,
})
export class ChildAComponent {
constructor(
private service: Service
) {
service.notifier.pipe(takeUntilDestroyed()).subscribe({
next: data => {
if (!data) return;
this.service.fetchData(data)
},
})
}
}
(Note the usage of takeUntilDestroyed from Angular rxjs-interop, which helps in automatically unsubscribing when the component gets destroyed.)
Employing a Subject enables us to utilize the AsyncPipe to directly access its value in templates, similar to how we reference a signal:
@Component({
selector: 'app-child-b',
template: `
<div>
@if (service.notifier | async; as data) {
{{ data }}
}
</div>
`,
standalone: true,
imports: [
CommonModule
]
})
export class ChildBComponent {
constructor(
protected service: Service
) {
}
}
If transitioning to an RxJS Subject is not feasible, one alternative approach involves using the effect hook to execute logic when the signal's value changes. Below is an example utilizing a signal:
@Injectable({
providedIn: 'root'
})
export class Service {
public notifier = signal<string | undefined>(undefined);
public fetchData(data: string) {
// business logic here
}
}
@Component({
selector: 'app-parent',
template: ``,
standalone: true,
})
export class ParentComponent implements OnInit {
constructor(
private service: Service
) {
}
ngOnInit() {
this.service.notifier.set("NewValue");
}
}
@Component({
selector: 'app-child-a',
template: ``,
standalone: true,
})
export class ChildAComponent {
constructor(
private service: Service
) {
effect(() => {
const value = this.service.notifier();
if (!value) return;
untracked(() => {
this.service.fetchData(value);
})
});
}
}
The effect within ChildAComponent triggers every time the notifier signal updates (tracking all signals used within it). We encapsulate our operations within the untracked callback to prevent potential side effects from hidden signals.
Edit: toObservable
Another solution could involve implementing the toObservable function from Angular rxjs-interop.
In the context of the previously mentioned Service, the implementation would appear as follows:
@Component({
selector: 'app-child-a',
template: ``,
standalone: true,
})
export class ChildAComponent {
constructor(
private service: Service
) {
toObservable(this.service.notifier).pipe(takeUntilDestroyed()).subscribe({
next: (value) => {
if (!value) return
this.service.fetchData(value);
}
});
}
}