angularsignalsimmutabilityangular-signals

Best way to expose a readonly signal array in an angular service


I am using Angular signals in a service

@Injectable()
export class MyService {
  private _myObjects = signal<MyObject[]>([]);
}

I want to expose this array to components in a read-only way. I see two approaches:

myObjects = this._myObjects.asReadonly();

and

myObjects = computed(() => myDeepCopy(this._myObjects()));
// where myDeepCopy is whatever deep copy function you can imagine

The first approach does not actually protect against mutation of the underlying array in the component but it is faster as I don't need to create a new deep copy each time _myObjects changes. Moreover, because it is marked as readOnly, I feel like it implies that a consumer should not try to mutate the underlying array (even if it does not enforce this rule).

So my question is: What is the best practice here? Should I just mark it as readOnly and trust the consumer not to mutate the underlying array or should I expose a deep copy so that there is absolutely no risk even if it comes at a small performance penalty and a bit less readable code?


Solution

  • The first method is correct:

    myObjects = this._myObjects.asReadonly();
    

    It avoid signal setting methods like set and update these are the only valid ways to write to a signal.

    Apart from this, all other methods of mutating the signal inner values, should be caught in code review of PRs and general checking of developer code, there is no inbuilt way to stop this.

    In Addition. The below is a valid pattern in signals, which cannot be done if you want to prevent signal indirect mutations.

    private _myObjects = signal<MyObject[]>([]);
    
    ngOnInit() {
        this._myObjects.set([{test: 1}]);
    }
    
    updateData() {
      const objects = this._myObjects();
      objects.forEach((item: any) => {
        item.test = true;
      });
      this._myObjects.set([...objects]);
    }
    
    updateDataInPlace() {
      this._myObjects().update((objects: any) => {
        objects.forEach((item: any) => {
          item.test = true;
        });
        return [...objects];
      });
    }