computed: производные значения

computed — это сигнал, который ничего не хранит, а вычисляет себя из других сигналов и пересчитывается ровно тогда, когда нужно.

«Не храните то, что можно вывести. Цена с учётом скидки — не данные, а формула».

Часть состояния не нужно хранить — её можно вычислить из другого. Сумма корзины выводится из списка товаров; полное имя — из имени и фамилии. Для таких производных значений есть computed(): он создаёт сигнал, формула которого зависит от других сигналов и пересчитывается автоматически.

import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-cart-summary',
  standalone: true,
  template: `
    <p>Товаров: {{ count() }}</p>
    <p>Сумма: {{ total() }} ₽</p>
    <p>К оплате: {{ withDiscount() }} ₽</p>
  `,
})
export class CartSummaryComponent {
  prices = signal([2990, 1990, 500]);
  count = computed(() => this.prices().length);
  total = computed(() => this.prices().reduce((a, b) => a + b, 0));
  withDiscount = computed(() => Math.round(this.total() * 0.9));
}

Здесь withDiscount зависит от total, а total — от prices. Измените prices — и вся цепочка пересчитается сама. Computed-сигнал доступен только для чтения: его нельзя set, ведь его значение полностью определяется источниками.

Как работает под капотом

Computed обладает двумя свойствами: он ленив (формула не выполняется, пока значение не прочитают) и мемоизирован (запоминает результат и пересчитывает только если изменилась хотя бы одна зависимость). Angular строит граф: знает, что withDiscount зависит от total, а тот от prices. При изменении источника помечаются «грязными» только затронутые узлы.

   prices (signal)
        |
        v
   total = computed(...)      пересчёт только при смене prices
        |
        v
   withDiscount = computed(...)  пересчёт только при смене total
        |
        v
   шаблон читает withDiscount() -> ленивое вычисление + мемо

Запускаемая врезка: computed с мемоизацией на JS

Покажем ленивость и мемо «руками». «Попробуй сам ▶».

function signal(v){ const s=()=>s.v; s.v=v; s.set=x=>{s.v=x; s.dirty&&s.dirty();}; return s; }

function computed(fn, deps){
  let cached, valid = false;
  deps.forEach(d => { d.dirty = () => { valid = false; }; });
  return () => {
    if (!valid) { cached = fn(); valid = true; console.log('  пересчёт'); }
    return cached;
  };
}

const prices = signal([100, 200]);
const total = computed(() => prices().reduce((a,b)=>a+b,0), [prices]);

console.log('total:', total());  // пересчёт + 300
console.log('total:', total());  // мемо: без пересчёта -> 300
prices.set([100, 200, 50]);
console.log('total:', total());  // пересчёт + 350

Частые ошибки

  • Пытаться записать computed. У него нет set — он только читается.
  • Класть в computed побочные эффекты. Формула должна только вычислять; для эффектов есть effect.
  • Хранить выводимое в обычном сигнале и руками синхронизировать — это источник рассинхрона.

Best practices

  • Всё, что выводится из другого состояния, делайте через computed, а не дублируйте.
  • Держите формулы чистыми — без запросов в сеть и логирования.
  • Стройте цепочки computed: это читаемо и пересчитывается ровно по необходимости.

Итоги. computed выводит значение из других сигналов, пересчитываясь лениво и с мемоизацией. Он read-only и не должен иметь побочных эффектов. Для побочных эффектов есть effect — про него дальше.

Закрепляем

Золотое правило: не храните то, что можно вывести. Сумма корзины, полное имя, флаг «есть ли ошибки» — это не данные, а формулы от других данных. Для таких производных значений создан computed(): он описывает формулу, автоматически отслеживает, от каких сигналов зависит, и пересчитывается, только когда хотя бы один из источников изменился. Это избавляет от целого класса багов рассинхрона, когда вы храните копию данных и забываете её обновить.

Два свойства computed стоит запомнить накрепко. Он ленив: формула не выполняется, пока кто-нибудь не прочитает значение — нет читателей, нет и работы. И он мемоизирован: результат кешируется и пересчитывается лишь при изменении зависимостей, поэтому многократное чтение дёшево. Из этого следует и ограничение: computed обязан быть чистым — только вычислять значение, без запросов в сеть, логирования и прочих побочных эффектов. Для эффектов есть отдельный инструмент, и смешивать их роли — верный путь к трудноуловимым ошибкам.

СвойствоЧто означает
ПроизводныйВыводится из других сигналов
ЛенивыйСчитается только при чтении
МемоизированныйКеширует, пересчёт по зависимостям
Read-onlyНельзя set, только чтение
Проверьте себя
1. Что особенного в computed-сигнале?
AЕго можно менять через set
BОн вычисляется из других сигналов, ленив, мемоизирован и доступен только для чтения
CОн работает быстрее обычного signal всегда
DОн хранит данные в localStorage
2. Почему в computed нельзя класть побочные эффекты (запросы, логи)?
AЭто запрещено TypeScript
Bcomputed предназначен только для чистого вычисления значения; для эффектов есть effect()
CЭффекты замедляют рендер
DТак нельзя в JavaScript