Computed signal dependencies

697 views Asked by At

Question to the Angular experts about Signals:

When you create a computed() signal, dependencies are automatically tracked, but according to the docs only for signals that were previously read.

If I understand this correctly, would the following be problematic since the every could potentially shortcircuit if the one of the first items is not selected, making the computed function only track a subset of the items?

const allSelected = computed(() => {     
    return selectableItems.every(item => item.selected()); 
});

To be clear: item.selected() is a signal as well.

It seems to be running fine, but I want to be sure.

1

There are 1 answers

0
Daniel Gimenez On

Your particular issue of concern shouldn't be an issue. For selectableItems to be true all items will have to be evaluated. If an item turns to false, it doesn't matter if an item with a greater index becomes false in the future because logically the result of every will still be the same.

That being said, you will have issues if selectableItems can have items added or removed.

An extreme example is if selectableItems is empty when computed is called. At that point, no signals will fire and the value will never be checked again.

A less extreme issue would be if an item is added at a later point. The value of allSelected won't be evaluated until one of the previous items have their value changed. If all the old items are true and the new item is false, then allSelected will be wrong.

If selectableItems can change, then a better design might be to make selectableItems a signal instead of all the individual items, and then use update when an item is changed.

// changed selectableItems to items to make example shorter.
items = signal([] as SelectableItem[]);

const allSelected = computed(() => this.items().every(item => item.selected));
addItem(item: SelectableItem) {
  this.items.update(x => [...x, item]);
}
updateItem(item: SelectableItem) {
  this.items.update(x => {
    const index = x.indexOf(item);
    return [...x.slice(0, index), item, ...x.slice(index + 1, 0)];
  });
}

Advanced Solution

If you wanted to keep the logic of your array outside of your component you could even create your own signal. The solution below adds methods to a writable signal and uses undocumented but exported functions from core signal primitives.

export interface ArraySignal<T> extends WritableSignal<T[]> {
  updateElement(item: T, updateFn: (x: T) => T): void;
  push(item: T): void;
}

export function arraySignal<T>(initialValue: T[]): ArraySignal<T> {
  const internal = signal(initialValue) as SignalGetter<T[]> & WritableSignal<T[]>;
  const node = internal[SIGNAL];
  return Object.assign(internal, {
    push: (item: T) => signalMutateFn(node, (x) => {
      x.push(item);
      return x;
    }),
    updateElement: (item: T, updateFn: (x: T) => T) => {
      signalMutateFn(node, (x) => {
        const index = x.indexOf(item);
        (index !== -1) && (x[index] = updateFn(item));
      })
    }
  })
}