Benoît Dumont
Benoît Dumont

Reputation: 25

Writing to signals is not allowed in a `computed` or an `effect` without using computed or effect

I am new to Angular / Signals / Zoneless and I face a strange error that i can't figure out. I got the Writing to signals is not allowed in a computed or an effect.

I have a signals which is an array of objects including an 'id' property and some methods. I build a get() function that check the signal's array for the specific id, if present it return the object, otherwise it create it and add it to the signal's array, then return it.

This get method work perfectly as long as I am not using it in the template. In the template I get the Writing signals error which I don't understand.

To keep methods on my signal's array's objects I used that 2 lines trick. (The need of methods prevent me from using the spread operator):

this.table().push(newTest);
this.table.set(this.table().slice());

here is the error on stackblitz:

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [JsonPipe],
  template: `
  <pre>
  Length: {{ table().length }}
  {{ get(1) | json }}
  <!-- {{ get(2) | json }}  uncomment to see the error in console-->
  </pre>
  `,
})
export class App {
  table = signal(<Test[]>[]);

  constructor() {
    this.get(1);
  }

  // look for the element in table and return it
  // if not present, create it, add it and return it
  get(id: number): Test {
    let myTest = this.table().find((elem) => elem.id == id);
    if (myTest != undefined) return myTest;
    let newTest = new Test(id);
    // the 2 next line is the only way I foound to add an object with methods in a signal array
    this.table().push(newTest);
    this.table.set(this.table().slice());
    return newTest;
  }
}

class Test {
  id: number;

  constructor(id: number) {
    this.id = id;
  }

  someMethod() {
    console.log(this.id);
  }
}

If I delayed the signals set line with a setInterval it works, but I am sure it will goes to terrible side-effects if I keep it like that.

This problem doesn't appear if I use object without methods, but I greatly need them. I mean interfaces instead of classes.

Upvotes: 1

Views: 1099

Answers (3)

Kobi Hari
Kobi Hari

Reputation: 1258

Angular uses effects when rendering your template, which means that if you bind to a method (which is a bas idea) in your template, and the method updates a signal, you should expect to get this error.

Recommendation:

  1. Instead of the get method, try to use a computed signal. (Again, here too, you will not be able to do things like modifying a signal)
  2. It seems like you are trying to do some sort of caching, so that when the template asks for test number 2, you check if it was generated before, and if not, you create it. The way to do it is to use some sort of stateful service in the background that holds the cache and provides the test as observable, which you can then convert to signal using the toSignal function.

Upvotes: 1

Beno&#238;t Dumont
Beno&#238;t Dumont

Reputation: 25

Thx for your answer but I can't pre-set the value cause it come from an HTTP call.

on my server I got a Table1 with lines pointing on Table2 I load a bunch of line from Table1, displaying them and get on the fly line from Table2 ( via services )

But as I was managing to reproduce the error without all of that, I asked on a simplier example

Moreover in my app I got a WritableSignals of a WritableSignals array of object

table = <WritableSignal<WritableSignal<Test>[]>> signal([])

that way I can trigger ChangeDetection only for the component that changed in zoneless mode, and trigger if a new component is added. But I might not be familiar enougth with angular / zoneless / signals to do things right...

Upvotes: 0

Naren Murali
Naren Murali

Reputation: 57756

Not an angular expert, but my wild guess is that the signal uses, effect internally to update the HTML, when the signal is read in HTML, hence when you update the signal using set it shows the error, since signal updates are not allowed inside effect.

I think is approach is wrong, since you should prepare the data of the signal on the ngOnInit or ngAfterViewInit and not on methods used in the DOM.

So you can initialize the values using a separate method setValue, for all the elements of the array.

constructor() {
  this.setValue(1);
  this.setValue(2);
}

setValue(id: number) {
  let newTest = new Test(id);
  this.table.update(value => ([...this.table(), newTest]));
}

Then when accessing the signal, you will always get the data hence you can rewrite the get method to.

get(id: number): Test | undefined {
  let myTest = this.table().find((elem) => elem.id == id);
  if (myTest != undefined) return myTest;
  return undefined;
}

Full Code:

import { bootstrapApplication } from '@angular/platform-browser';
import {
  Component,
  provideExperimentalZonelessChangeDetection,
  signal,
} from '@angular/core';
import { JsonPipe } from '@angular/common';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [JsonPipe],
  template: `
  <pre>
  Length: {{ table().length }}
  {{ get(1) | json }}
  {{ get(2) | json }} 
  </pre>
  `,
})
export class App {
  table = signal(<Test[]>[]);

  constructor() {
    this.setValue(1);
    this.setValue(2);
  }

  setValue(id: number) {
    let newTest = new Test(id);
    // the 2 next line is the only way I foound to add an object with methods in a signal array
    this.table().push(newTest);
    this.table.set(this.table().slice());
  }

  // look for the element in table and return it
  // if not present, create it, add it and return it
  get(id: number): Test | undefined {
    let myTest = this.table().find((elem) => elem.id == id);
    if (myTest != undefined) return myTest;
    return undefined;
  }
}

class Test {
  id: number;

  constructor(id: number) {
    this.id = id;
  }

  someMethod() {
    console.log(this.id);
  }
}

bootstrapApplication(App, {
  providers: [provideExperimentalZonelessChangeDetection()],
});

Stackblitz Demo

Upvotes: 0

Related Questions