Руна $derived: производные значения

Руна $derived вычисляет значение из другого состояния и пересчитывается только тогда, когда нужно.

«Если значение можно вычислить — не храните его, выводите.» $derived избавляет от рассинхрона между связанными данными.

Часто одно значение зависит от другого. Полная цена корзины зависит от списка товаров. Полное имя зависит от имени и фамилии. Хранить такие значения в отдельном $state и руками их синхронизировать — источник багов: легко забыть обновить производное при изменении исходного. Руна $derived решает это: вы объявляете формулу, а Svelte сам пересчитывает результат, когда меняются зависимости.

$derived в Svelte 5 заменяет реактивные объявления $: из четвёртой версии. Ключевое свойство — мемоизация: выражение пересчитывается, только когда реально изменилась одна из его зависимостей, а не на каждый чих. Если ничего не поменялось, используется закешированное значение.

<script>
  let price = $state(100);
  let quantity = $state(3);

  let total = $derived(price * quantity);

  // для сложных вычислений есть $derived.by с телом функции
  let label = $derived.by(() => {
    return total > 250 ? 'Дорого' : 'Норм';
  });
</script>

<p>Итого: {total} ({label})</p>

Здесь total автоматически следует за price и quantity. Меняете количество — итог пересчитывается. А label следует уже за total. Получается цепочка зависимостей, которую Svelte отслеживает за вас. Вариант $derived.by нужен, когда вычисление не помещается в одно выражение.

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

Смоделируем производное значение на чистом JS. Идея: производное — это функция, которая помнит свои зависимости и пересчитывается лениво, только когда к ней обращаются после изменения источника.

// Упрощённая модель $derived с мемоизацией
function makeReactive(initial) {
  let v = initial; const subs = new Set();
  return { get:()=>v, set:(n)=>{v=n; subs.forEach(f=>f());}, sub:(f)=>subs.add(f) };
}
function derived(fn, deps) {
  let cache, dirty = true;
  deps.forEach(d => d.sub(() => { dirty = true; })); // пометить грязным при изменении
  return { get() {
    if (dirty) { cache = fn(); dirty = false; console.log('пересчёт!'); }
    return cache;
  }};
}

const price = makeReactive(100), qty = makeReactive(3);
const total = derived(() => price.get() * qty.get(), [price, qty]);

console.log('итого:', total.get()); // пересчёт! -> 300
console.log('итого:', total.get()); // из кэша -> 300 (нет пересчёта)
qty.set(5);
console.log('итого:', total.get()); // пересчёт! -> 500

Попробуй сам ▶ — вставь код в консоль браузера (F12 → Console) и нажми Enter, чтобы увидеть вывод.

Обратите внимание на лог: второй вызов total.get() не пересчитывает значение — оно берётся из кэша. Пересчёт случается только после qty.set(5). Это и есть мемоизация, которую $derived даёт бесплатно.

  price ($state) --+
                   |--> total ($derived) --> label ($derived) --> разметка
  qty   ($state) --+

  меняется price/qty -> total помечен грязным -> пересчёт при чтении

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

  • Класть в $derived побочные эффекты. Запись в состояние и сетевые вызовы запрещены — для них есть $effect.
  • Дублировать данные. Если значение выводится из другого, не храните его в отдельном $state.
  • Путать $derived(expr) и $derived.by(() => ...). Первый берёт выражение, второй — функцию.

Best practices

  • Правило выбора: вычисляете значение — берите $derived; выполняете действие — берите $effect.
  • Держите выражения внутри $derived чистыми и без сайд-эффектов.
  • Стройте цепочки производных вместо ручной синхронизации связанных значений.

Цепочки вычислений в реальных интерфейсах

В настоящих приложениях производные значения редко существуют поодиночке — они выстраиваются в цепочки. Представьте корзину интернет-магазина: из списка товаров выводится подытог, из подытога и промокода — скидка, из подытога и скидки — итоговая сумма, из суммы — доступность кнопки оформления. Каждое звено описано как $derived от предыдущих, и когда пользователь меняет количество одного товара, Svelte пересчитывает ровно те звенья, которых это касается, и не трогает остальные. Без производных вам пришлось бы вручную пробрасывать изменение через всю цепочку, и любая забытая строка синхронизации стала бы багом. С $derived вы описываете отношения между данными один раз и декларативно — а движок сам поддерживает их в согласованном состоянии. Это превращает класс утомительных и хрупких задач в простое объявление формул.

Итог: $derived описывает значение как формулу от состояния. Svelte мемоизирует результат и пересчитывает его только при изменении зависимостей, избавляя вас от рассинхрона.

Проверьте себя
1. Для чего предназначена руна $derived?
AДля выполнения побочных эффектов вроде запросов
BДля вычисления значения из другого состояния с мемоизацией
CДля объявления пропсов компонента
DДля подписки на DOM-события
2. Что запрещено делать внутри выражения $derived?
AЧитать другое реактивное состояние
BВыполнять арифметику
CМенять состояние и выполнять побочные эффекты
DВозвращать строку