Soumalya Bhattacharya
Soumalya Bhattacharya

Reputation: 674

How to run a function when a signal value changes in Angular v 17

I have a parent component and there are multiple child components and some components which are not direct descendants. There is one data which is taken as input from user in parent component.

Whenever the input value is changed in the parent component a function is called in service layer that sets the input value in a signal. this signal type variable is a class variable of that service class. The type of the class variable is WritableSignal<Number> and it is set by using .set() method when ever the input value is changed.

All the Signal related things are imported from @angular/core.

All the child components and indirect descendants of the parent component have access to the service class and needs this signal type variable to do data fetching from backend.

I want whenever the value of the input variable changes and in turn it updates the signal value all the child components and other components that use this signal should run their function to do the data fetching again.

How can I do that.

Upvotes: 5

Views: 6076

Answers (3)

Redict
Redict

Reputation: 21

The trick is to add a reference to the signal of your parent class in the child. This will let you register the effect callback. As you can see in the example below.

//Signal service
@Injectable({providedIn: 'root'})
export class SignalService {

  private readonly rawSignal = signal(0)
  public accessableSignal = this.rawSignal.asReadonly();

  public updateSignal() {
    this.rawSignal.update(value => value + 1);
  }
}

// foo
@Component({
  selector: 'app-foo',
  standalone: true,
  imports: [
    FooChildComponent
  ],
  template:`
    <button (click)="emitSignal()">Click Me</button>
    <app-foo-child/>`,
})

export class FooComponent {
 private signalService= inject(SignalService);

 emitSignal(){
   this.signalService.updateSignal();
 }
}

//foo-child
@Component({
  selector: 'app-foo-child',
  standalone: true,
  template: `Signal value received {{ counter }}`,
})
export class FooChildComponent {
    
counter = 0;
private signalToHandle = inject(SignalService).accessableSignal;
    
constructor() {
    effect(() => this.counter = this.signalToHandle());
    }
}

I hope this will help you. Regards

Upvotes: 2

Tib&#232;re B.
Tib&#232;re B.

Reputation: 1226

I would advise you to replace the Signal used in your service layer by either a Subject or BehaviourSubject.

An RxJS Subject is a special case of Observable that also doubles as an Observer, meaning that we can manually feed data into it, which is very handy when we want to notify multiple listeners at the same time.

In my example I will use a BehaviourSubject, a special type of Subject that takes a default value and that holds the last emitted value for future subscribers (which guarantees that it always emits a value when we subscribe to it.)

Consider the following service :

@Injectable({
  providedIn: 'root'
})
export class Service {
  public notifier = new BehaviorSubject<string | undefined>(undefined);

  public fetchData(data: string) {
    // business logic here
  }
}

The components who need the Subject's value to fetch the remote data can now subscribe to it to be notified, and the parent component can use the .next function the set a new 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 use of takeUntilDestroyed from Angular rxjs-interop, which that can be used inside an injection context to automatically dispose of the subscription when the component will be destroyed.)

The use of a Subject allows use to leverage the AsyncPipe to use its value directly from a template like we would do with 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
  ) {
  }
}

But if for any reason the switch to an RxJS Subject would not be possible, you could make use of the effect hook to run logic when a signal's value changes, consider the following example now using 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 used inside ChildAComponent will now run every time the notifier signal changes (The effect keeps track of every signal we use inside of it and will run when the value of any of them changes).

We then run our logic inside the untracked callback to prevent angular from tracking any other signals that may be hidden behind our functions and could cause some side effect.

Edit: toObservable

An other solution may be to make use of the toObservable function from Angular rxjs-interop.
Considering the previously illustrated Service, the usage would be 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);
      }
    });
  }
}

Upvotes: 6

RMorrisey
RMorrisey

Reputation: 7739

I'm not that familiar with Signals but it looks like only the angular component template can listen for Signal values. The docs for Signals are in feature preview, so it may not be fully fleshed out yet.

You can accomplish what you want using a Subject or BehaviorSubject with RxJS.

The component with the input can call: subject.next(new value)

and the listening components can listen as follows:

subject.asObservable().pipe(
   switchMap((newValue) => {
     return this.httpClient.get...
   )
)

This will return an Observable. If you want to display the result from the API call in your child component, you can set the Observable as a field on the component class and use the | async pipe to display it in your Angular template.

If you need to do some other operation with the value (and you are not able to do it in your pipe) you can use the observable.subscribe(...) method to listen for changes. Be sure to unsubscribe() in your ngOnDestroy method if you do this.

You may also find some other rxjs operators helpful, e.g. distinctUntilChanged() to ignore repeat values from the input, and shareReplay() to prevent the API call from being fired multiple times for each child component listening.

Upvotes: 0

Related Questions