Практическая работа к уроку 5 (занятие 2)

Сигналы и реактивное состояние

Замените ручные пересчёты на signal, computed и effect: производные значения пересчитываются сами.

В этом уроке мы разберём четыре понятия. Чтобы не путаться, сразу дадим им «человеческие» имена:

Что вы узнаете

Задание 1. Состояние и производные значения

Список задач — сигнал. Количество активных и выполненных — computed.

Контекст. В проекте TaskFlow (список задач) мы храним все задачи в одном сигнале. А счётчики «активно» и «готово» хотим видеть без ручного подсчёта — пусть они считаются сами.

💡 Аналогия из жизни. Сигнал tasks — это как группа в мессенджере: изменил сообщение — и всем подписчикам приходит уведомление. А computed — это итоговый счёт: производное значение, которое считается само, как итог в чеке, когда ты добавляешь товары.

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

// Сигнал tasks — наш список задач (доска объявлений).
// Тип Task[] значит: «массив задач».
const tasks = signal<Task[]>([
  { id: 1, title: "Изучить сигналы", completed: false },
  { id: 2, title: "Написать computed", completed: true }
]);

// computed — производное значение. Оно смотрит на tasks()
// и считает, сколько задач ещё НЕ выполнено (completed === false).
const activeCount = computed(() =>
  tasks().filter(t => !t.completed).length
);

// Аналогично — сколько задач УЖЕ выполнено.
const doneCount = computed(() =>
  tasks().filter(t => t.completed).length
);

// При любом изменении tasks() счётчики пересчитаются сами.
// update() берёт старый список и возвращает НОВЫЙ (неизменяемо).
tasks.update(list =>
  [...list, { id: 3, title: "effect()", completed: false }]
);

Пошаговый разбор:

  1. Создаём сигнал tasks с двумя задачами. Пока активна 1, выполнена 1.
  2. Создаём activeCount и doneCount — они «подписаны» на tasks.
  3. Вызываем tasks.update(...): берём старый список, добавляем третью задачу через [...list, ...] (спред — копия + новый элемент).
  4. Angular видит изменение сигнала и сам пересчитывает оба счётчика.
Проверка. После update значение activeCount() станет 2 без ручного пересчёта — вы просто читаете activeCount(), а число уже правильное.

Задание 2. Фильтрация через computed

Видимый список зависит от выбранного фильтра.

Контекст. В TaskFlow есть кнопки «Все / Активные / Готовые». Вместо того чтобы вручную перестраивать список при каждом нажатии, мы делаем видимый список производным от фильтра.

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

// Сигнал filter хранит текущий выбор: 'all' | 'active' | 'done'.
// done здесь — ТОЛЬКО значение фильтра (какие задачи показывать),
// а не поле задачи «выполнена». В TaskFlow поле задачи называется completed.
const filter = signal<'all' | 'active' | 'done'>('all');

// visibleTasks — что именно показываем на экране.
// Зависит и от tasks(), и от filter().
const visibleTasks = computed(() => {
  const f = filter();          // читаем текущий фильтр
  if (f === 'done')   return tasks().filter(t => t.completed);   // только готовые
  if (f === 'active') return tasks().filter(t => !t.completed);  // только активные
  return tasks();              // 'all' — показываем всё
});

Пошаговый разбор:

  1. Создаём сигнал filter со значением 'all' по умолчанию.
  2. Внутри computed читаем filter() — этим мы «подписываемся» на него.
  3. По значению фильтра возвращаем нужную часть списка.
  4. Когда пользователь меняет фильтр (filter.set('done')), visibleTasks пересчитается сам.
Важно про имена. Поле задачи — completed (выполнена ли). А 'done' — это просто одно из значений фильтра, выбранное пользователем. Не путайте их: completed отвечает на вопрос «задача готова?», а filter()==='done' — на вопрос «что сейчас показываем?».

Задание 3. effect для побочных эффектов

Реагируйте на изменения, не возвращая значение (лог, синхронизация).

Контекст. Иногда при изменении данных нужно сделать что-то «сбоку»: записать в лог, сохранить в браузере, обновить заголовок. Для этого — effect(), а не computed() (который возвращает значение).

💡 Аналогия из жизни. effect — как будильник. Ты не спрашиваешь у будильника «сколько времени» (он ничего не возвращает), ты просто говоришь: «когда время изменится — разбуди меня». Так же effect: «когда tasks меняется — сделай это».

effect(() => {
  // Этот код запускается каждый раз, когда меняются
  // tasks() или activeCount() (мы их «читаем» внутри).
  console.log('Сейчас задач:', tasks().length,
              'активных:', activeCount());
});

Пошаговый разбор:

  1. Создаём effect с функцией-действием.
  2. Внутри читаем tasks() и activeCount() — значит, effect «подписан» на них.
  3. При любом изменении одного из них функция запускается снова и пишет в консоль.
Важно. effect() запускается только внутри контекста инъекции (в компоненте или сервисе), иначе Angular выбросит ошибку. Простыми словами: effect должен «жить» внутри Angular-компонента, а не в случайном скрипте.

Интерактивная песочница

Симуляция сигналов на чистом JS: обновляем состояние — и производные пересчитываются.

Контекст. Ниже — упрощённая модель сигналов без Angular, чтобы вы «пощупали» идею: есть значение, есть подписчики, и при изменении значения запускается перерисовка.

💡 Аналогия из жизни. Это как группа в мессенджере: у сигнала есть список подписчиков (subs). Меняешь значение (set) — и всем подписчикам приходит уведомление, один из которых перерисовывает экран (render).

▶️ Песочница. Нажмите «Запустить» — через секунду список обновится сам, без ручного вызова отрисовки.
Песочница: нажми «Запустить» — и увидишь, как список сам обновится с «2 задач» до «3 задач» через секунду. Меняй код и запускай снова.
Песочница: signal / computed

Что увидит студент. Сначала на экране: «Всего: 2, активных: 1». Через секунду текст сам сменится на «Всего: 3, активных: 2» — никто не вызывал render() вручную, это сделал сигнал.

Проверьте себя

Вопросы.
  1. Чем computed отличается от обычной функции? (Подсказка: обычную функцию надо вызвать заново и она не знает, когда данные изменились.)
  2. Почему обновлять состояние нужно неизменяемо (через spread [...list, ...])? (Подсказка: чтобы сигнал «заметил» изменение и оповестил подписчиков.)
  3. В каком контексте разрешён effect()? (Подсказка: внутри компонента или сервиса Angular.)

Практика к уроку 5. Далее: Практика к уроку 6 →

Куда дальше: теория сигналов — в Уроке 5; три состояния UI — в лабораторной №2.

📖 Глоссарий

ТерминЖизненная аналогияКоротко
signalГруппа в мессенджере: изменил сообщение — и всем пришло уведомление.Переменная с подписчиками, которые узнают об изменении.
computedИтоговый счёт в чеке: меняешь товары — сумма считается сама.Производное значение, пересчитывается автоматически.
effectБудильник: не отвечает, а просто будит при смене времени.Действие «сбоку» при изменении данных, ничего не возвращает.
Состояние (state)Статус заказа: «в обработке / готов / ошибка».Текущее положение дел в приложении.
НеизменяемостьНовый снимок вместо правки оригинала: новый кадр, а не рисунок поверх.Создаём новый объект, а старый не трогаем.
filterСитечко для чая: пропускает только нужное, остальное — за бортом.Оставляет в списке лишь элементы по условию.
mapКонвейер: каждая деталь проходит обработку и выходит преображённой.Превращает каждый элемент списка по правилу.

🧠 Самопроверка

1. Сигнал сам оповещает подписчиков при изменении значения. Верно или неверно?

2. computed нужно вызывать заново каждый раз, чтобы получить свежее значение. Верно или неверно?

3. Чтобы сигнал «заметил» изменение, список задач лучше менять неизменяемо (через spread). Верно или неверно?

Прогресс курса