David
David

Reputation: 63

Angular effect() is not getting triggered on state changes from Observable that are updating the component

I am trying to work with Angular 17 effects using the toSignal() interop class to convert an Observable to a signal. I am registering an effect for that signal in the constructor, and I am expecting to see the effect trigger every time next is called on the BehaviorSubject

What I am seeing is that the signal called in the for loop of the template is updating as I would expect, and a new item gets rendered to the list. However, the effect method to log out the changes is only being called once.

@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [],
  template: `
    <button (click)="updateState()">
      Update State
    </button>

    <ol>
      @for (s of signalState(); track s) {
        <li>{{s}}</li>
      }
    </ol>
  `,
  styles: ``
})
export class DashboardComponent {
  testBehaviorSubject = new BehaviorSubject<string[]>([]);
  testObservable = this.testBehaviorSubject.asObservable();
  signalState = toSignal(this.testObservable);
  history: string[] = [];

  constructor() {
    effect(() => console.log(`State updated: `, this.signalState()));
  }

  updateState() {
    this.history.push('Clicked');
    this.testBehaviorSubject.next(this.history);
  }
}

The DOM is updated when the button is clicked

Why are the effects not firing in the same way?

Upvotes: 6

Views: 3648

Answers (1)

user680786
user680786

Reputation:

  1. DOM event click schedules a Change Detection cycle;
  2. While detecting changes for this component, @for reads the expression s of signalState() and creates output for the current value of the signal (so the list will always be re-rendered correctly). @for does NOT listen to signal changes, it just returns a new output for the new input;
  3. In updateState(), you are pushing the same array as the next value. Signals will only notify its consumers when the new value is actually new. To check it, a signal compares values using Object.is() (if no custom equality check function is set). Because it is the same array, Object.is() will return true and the signal will not notify consumers (effect(), in your code) that the value is changed1.

You have 2 options:

  1. Use immutable structures with signals (recommended way), here is an example of how to easily achieve it in your code;
  2. Use custom equality check. Only use it when you can not use immutable structures.

1 Even more - signal will not set the new value, if the equality check function shows that the new value is equal to the old one. But in your case, you are mutating the array by reference (history.push()). The signal can not see this mutation but the array itself will be mutated anyway - that's why @for receives all the items, even if the signal skipped the change.

Upvotes: 5

Related Questions