Konrad Viltersten
Konrad Viltersten

Reputation: 39210

Effect in parent has no effect despite change in signal in child

In the child, I have a field data and on a click I alter contents of it. Then, I want an effect in the parent to fire detecting said change. For some reason, the effect doesn't fire when I alter the model input in the child (however, the effect fires when setting the signal in the parent.

The child has this.

export class ChildComponent {
  data = model({} as Vm);
  onClick() {
    console.log("Click alters the data.");
    // this.data.set(this.data());
    this.data.update(a => a);
  }
}

The parent has this.

export class ParentComponent {

  constructor() {
    interval(1000).subscribe(a => console.log("poll: ", this.data()));
    effect(() => { console.log("effect: ", this.data()) });
  }
  data = signal({} as Vm);
  ngOnInit() { this.data.set(this.getData()); }
}

I see the intial effect displaying an empty object. Then, I see the update from parent's init method. I also see the secondwise check displaying the value altered in the child. Yet - the effect won't fire again, only the aforementioned twice.

I'm out of ideas on how to troubleshoot it further. Checking the official docs, I see that it's supposed to work (which it, sort of, does!) but I'm entirely certain if I perform the alteration in the child component fully correctly.

(In the actual scenario, I'll be only altering a field in an object in an array that is a part of the data model input but that's the next step. First, I want to understand why doing set(...) nor update(...) triggers the effect in the partent.

Upvotes: 0

Views: 371

Answers (1)

Naren Murali
Naren Murali

Reputation: 57696

The signal fires only when the actual value changes, since arrays and objects are stored as references in memory, if you change the internal properties the memory reference does not change, hence it won't be detected as a change. It also does not change when you just return the object as is.

Signals And Array Mutability In Angular 18 - Ben Nadel

GitHub issue - Angular 17 - Mutable signals are not supported

You can use object destructuring (for objects) or .slice() array method (only arrays) to create a new memory reference and this will be propagated to the parent. Since the memory reference is changed, it will be picked up by the signal as a new change.


This explanation applies only for effect and computed since they react only to signal changes.

Sometimes you might notice the HTML updating fine for inner properties of a signal containing an object ( e.g: {{ data()?.qwerty?.test }} ). This is not due to signals, but due to change detection running ( Click event, running cdr.detectChanges() ), Angular will fire a change detection cycle and the latest values are updated in html for the properties nested inside the signal. This does not mean that signals support mutable objects. It's simply a side effect of change detection running.


@Component({
  selector: 'app-child',
  standalone: true,
  template: `
    <button (click)="onClick()">click</button>
  `,
})
export class ChildComponent {
  data = model({});
  onClick() {
    console.log('Click alters the data.');
    // this.data.set(this.data());
    // object destructuring creates new memory reference.
    this.data.update((a: any) => ({ ...a }));
  }
}

Full Code:

import { Component, effect, model, signal } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { interval } from 'rxjs';
@Component({
  selector: 'app-child',
  standalone: true,
  template: `
        <button (click)="onClick()">click</button>
      `,
})
export class ChildComponent {
  data = model({});
  onClick() {
    console.log('Click alters the data.');
    // this.data.set(this.data());
    this.data.update((a: any) => ({ ...a }));
  }
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ChildComponent],
  template: `
    <app-child [(data)]="data" (dataChange)="dataChange($event)"/>
  `,
})
export class App {
  data = signal({});
  constructor() {
    // interval(1000).subscribe((a: any) => console.log('poll: ', this.data()));
    effect(() => {
      console.log('effect: ', this.data());
    });
  }
  ngOnInit() {
    // this.data.set(this.getData());
  }

  dataChange(e: any) {
    console.log(e);
  }
}

bootstrapApplication(App);

Stackblitz Demo

Upvotes: 2

Related Questions