🅰️ День 2
Что планируем освоить: Примитивы Сигналов (signal, computed, effect), новые сигнальные инпуты (input, input.required), двустороннее связывание через model() и выходные события output().
Забудьте про
useEffectс его бесконечными массивами зависимостей[deps]и проблемами с замыканиями (stale closures). В Angular 20+ Сигналы — это синхронный направленный ациклический граф (DAG). Когда вы вызываетеeffect()илиcomputed(), рантайм Angular во время выполнения автоматически считывает (трекает) все сигналы внутри функции. Никакого ручного управления зависимостями — система гарантирует glitch-free реактивность без лишних вычислений «из коробки».
Шаг 1: Примитивы Сигналов и замена useEffect через effect() (3 часа)
В React для сайд-эффектов (логирование, синхронизация с LocalStorage) вы использовали useEffect. В Angular 20+ эту роль выполняет функция effect(). Она запускается внутри конструктора или контекста внедрения (Injection Context) и автоматически реагирует на изменения атомов данных.
Генерируем изолированный компонент для первого эксперимента (сразу со всеми вашими DX-флагами):
yarn ng generate component components/day-2/step-1 --selector="exp-day-2-step-1" --standalone --inline-template --inline-style --skip-testsКак запустить и что проверить в браузере:
- Подключите этот компонент в
src/app/app.ts(добавьтеDay2Step1в массивimports) и выведите тег<exp-day-2-step-1></exp-day-2-step-1>в шаблонеapp.html. - Откройте консоль браузера. При первой загрузке вы увидите лог
🏗️ [Constructor], лог пересчета🟢 [Computed Nodes]и первичный запуск эффекта📡 [Effect Triggered]. - Нажмите кнопку «Мутировать холостой сигнал». В консоли появится лог клика, но сам эффект и computed-метод останутся в абсолютной тишине. В отличие от
useEffectбез указания зависимостей или внешних стейт-менеджеров, реактивность здесь изолирована хирургически на уровне атомов данных.
Шаг 2: Сигнальные инпуты input() и выходные события output() (3 часа)
Наша цель на этом шаге — полностью перестроить ментальную модель передачи пропсов (Props) и колбэков, к которой вы привыкли в React.
Компонентное взаимодействие (React Props vs Angular Signals)
В React пропсы — это объект, который полностью пересоздается при ререндере родителя, из-за чего дочерний компонент тоже вынужден перевызываться (если не закрыться с помощью React.memo).
В Angular 20+ входящие свойства — это сигнальные инпуты (input()). Они являются полноценными readonly-сигналами (Nodes в графе реактивности). Если родитель меняет значение пропса, Angular обновляет только конкретную текстовую ноду внутри дочернего компонента. Сам дочерний класс при этом не перерендеривается. Для передачи событий наверх используется новый компактный примитив output().
yarn ng generate component components/day-2/step-2 --selector="exp-day-2-step-2" --standalone --inline-template --inline-style --skip-testsCLI создаст один файл src/app/components/day-2/step-2/step-2.ts.
👉 Сделайте сами
Откройте созданный файл и попробуйте пошагово воспроизвести следующее:
- Создайте два компонента в таком порядке: дочерний
Day2Step2Childс селекторомexp-day-2-step-2-childи родительскийDay2Step2с селекторомexp-day-2-step-2(родительский будет импортировать дочерний, поэтому родительскийDay2Step2объявляем последним)- Поскольку у нас standalone-архитектура, и родитель (
Day2Step2) рендерит ребенка (Day2Step2Child), они должны знать друг о друге на уровне TS. Правило для одного файла: сначала объявляем дочерний компонент с декоратором, а затем родительский, который импортирует дочерний в свой массивimports). - В Angular каждый визуальный элемент (компонент) обязан иметь свой декоратор
@Component.Нельзя просто объявить пустой класс и ожидать, что он станет компонентом.
- Поскольку у нас standalone-архитектура, и родитель (
- В
src/app/app.tsимпортируем и подключаем только родительский - В разметку родительского
src/app/components/day-2/step-2/step-2.tsдобавляем тег дочернего, соответствующий его селектору - В разметку
src/app/app.htmlдобавляем только родительский (добавляем тег родительского, соответствующий его селектору) - Состояние родителя:
parentCoreName = signal('Main-Core'); parentFrequency = signal(3200); lastChildLog = signal('Пока нет сигналов'); - Состояние дочернего:
// Сигнальные инпуты (Props нового поколения) coreName = input.required<string>(); // Обязательный проп frequency = input<number>(2400); // Опциональный проп с дефолтным значением // Аналог useMemo, завязанный строго на инпут coreName derivedMetric = computed(() => { console.log('🟢 [Child Computed]: Пересчитываю метрику на основе coreName...'); return `[${this.coreName().toUpperCase()}]—SYS-OK`; }); // Новый компактный примитив для генерации событий наверх pingEvent = output<string>(); - Обработчики родительского:
changeCoreName() { this.parentCoreName.set(Math.random() > 0.5 ? 'Quantum-Core' : 'Matrix-Unit'); } changeFrequency() { this.parentFrequency.update(f => f + 200); } // Обработчик события от ребенка. Аргумент $event строго типизирован. onChildPing(message: string) { this.lastChildLog.set(message); console.log(`📡 [Parent Engine] Перехвачено событие: ${message}`); } - Обработчики дочернего:
sendPing() { this.pingEvent.emit(`Пинг из дочернего потока в ${new Date().toLocaleTimeString()}`); } - Передача сигналов родителя в инпуты ребенка через квадратные скобки
[prop]и слушателя событий ребенка через круглые скобки(event):<exp-day-2-step-2-child [coreName]="parentCoreName()" [frequency]="parentFrequency()" (pingEvent)="onChildPing($event)" />
Здесь объявлены оба класса с префиксом Day2Step2, наглядно демонстрирующие работу input(), input.required() и output():
Полный исходный код компонентов Day2Step2Child и Step2Component (а также инструкции по подключению в src/app/app.ts и src/app/app.html) вы можете найти в исходном документе. Ниже приведена краткая структура реализации:
Day2Step2Child— дочерний компонент, принимающий обязательный инпутcoreName = input.required<string>(), опциональныйfrequence = input<number>(2400), производное состояниеderivedMetricчерезcomputed, а также отправку событий наверх черезpingEvent = output<string>().Step2Component— родительский класс со структурой сигналов состояния и разметкой, связывающей родительские данные с дочерним узломexp-day-2-step-2-child.
Перечислю, что нового увидел React-разработчик:
- Синальный инпут
input(для дочернего компонента) в двух вариациях: обязательная и опиональная - Новый компактный примитив
outputдля генерации событий наверх - Спеифический проброс сигналов в "инпуты" и слушателей
Попробуйте кликнуть на «2. Изменить frequency родителя» и убедитесь в консоли, что лог 🟢 [Child Computed] молчит — Angular обновил только текстовый узел частоты, полностью проигнорировав остальной шаблон ребенка.
⚛️ Эквивалент на React
Разберем фундаментальный синтаксис связи компонентов в Angular. Для Senior React разработчика она поначалу выглядит чужеродно из-за обилия скобок, но в ней скрыта строгая и очень логичная система.
В эквивалентном примере на React вы передаете всё в одном объекте props, смешивая данные и колбэки:
<Day2Step2Child
coreName={parentCoreName} // Передача переменной (проп)
frequency={parentFrequency} // Передача переменной (проп)
onPingEvent={onChildPing} // Передача функции (колбэк)
/>В Angular потоки данных разделены на уровне синтаксиса с помощью скобок:
- Квадратные скобки
[ ]— это поток данных внутрь компонента. - Круглые скобки
( )— это поток событий наружу из компонента.
Разбор синтаксиса и закрепление материала
[coreName]="parentCoreName()"- Что значат
[ ]: Мы говорим компилятору Angular: «В дочернем компоненте есть сигнальный инпут с именемcoreName. Привяжись к нему». - Почему справа вызов функции
parentCoreName(): ПосколькуparentCoreName— это Сигнал в родительском классе, мы обязаны его вызвать, чтобы извлечь текущее значение. - Аналогия в React: Это передача обычного пропа сверху вниз. Изменится сигнал в родителе — дочерний инпут автоматически получит новое значение.
(pingEvent)="onChildPing($event)"- Что значат
( ): Мы говорим Angular: «Слушай событие (выходной поток) с именемpingEvent, которое генерирует дочерний компонент». - Что такое
$event: Это зарезервированное имя переменной в шаблонах Angular. В него рантайм автоматически упаковывает те данные, которые дочерний компонент выбросил наружу через метод.emit(data). В нашем случае туда прилетает строкаstring. - Аналогия в React: Это передача функции-колбэка
onPingEvent={data => onChildPing(data)}.
Как легко привыкнуть
[ Квадратные ] = Данные заходят (Вход). Представьте их как жесткие стены коробки (Box), в которую вы упаковываете данные и отправляете вниз дочернему элементу.
( Круглые ) = События вылетают (Выход). Представьте их как круглую кнопку (Button), на которую нажимают, или как взрывную волну события, которая летит наверх к родителю.
Таким обазом, если вы видите [property], вы сразу понимаете, что дочерний элемент зависит от состояния родителя. Если видите (action), вы сразу понимаете, что родитель реагирует на действия внутри дочернего элемента.
Чем это лучше?
Как Senior-разработчик, вы оцените это в рантайме:
- В React, когда вы пишете
<Component onClick={handleClick} />, при каждом ререндере родителя ссылка на функциюhandleClickможет пересоздаваться (если не обернуть её вuseCallback), что часто приводит к лишним ререндерам ребенка. - В Angular запись
(pingEvent)="onChildPing($event)"не пересоздает функции в памяти шаблона. Роутер событий (output()) настраивается компилятором один раз при инициализации. Вы можете мутировать состояние родителя как угодно, но рантайм никогда не будет заниматься холостым перевызовом дочерних обработчиков.
Шаг 3: Двустороннее связывание через model() (2 часа)
В React двустороннее связывание состояния с полем ввода (или дочерним компонентом) всегда пишется через явную связку свойства и колбэка: value={text} и onChange={e => setText(e.target.value)}.
В Angular 20+ появился революционный примитив model(). Он объединяет в себе свойства сигнального инпута (input()) и выходного события (output()). В шаблоне это порождает синтаксис, который в мире Angular называют «Бананом в коробке»: [(modelName)].
yarn ng generate component components/day-2/step-3 --selector="exp-day-2-step-3" --standalone --inline-template --inline-style --skip-tests🔬 Что потестировать в браузере:
- Инпут родителя: Меняйте значение в верхнем текстовом поле. Нижний компонент мгновенно подтягивает цифру.
- Инпут ребенка: Меняйте значение в нижнем поле. Вы увидите, что значение в родителе реактивно обновляется в ту же миллисекунду.
- Кнопки: Нажимайте кнопки сброса или форсирования. Данные синхронизируются во всех направлениях без единого холостого ререндера или перевызова функций классов.
Небольшое пояснение
В React нет встроенного механизма для связывания состояния с тегом <input>. Вам всегда нужно вручную писать value={state} и onChange={e => setState(...)}.
В Angular директива [(ngModel)] внутри FormsModule — это просто синтаксический сахар (макрос). Когда вы пишете:
<input [(ngModel)]="parentLevel" />Компилятор Angular разворачивает это в обычный проброс данных и слушатель события:
<input [ngModel]="parentLevel()" (ngModelChange)="parentLevel.set($event)" />FormsModule не занимается «невидимым пробросом» данных между вашими компонентами (родителем и ребенком). В примере Шага 3 данные между родительским Day2Step3 и дочерним Day2Step3Child пробрасывались абсолютно явно через синтаксис [(level)]="parentLevel".
Как работает DI (Аналог Context API без проброса пропсов)
Вместо того чтобы гонять сигналы через три уровня компонентов с помощью
[(model)]или[input], мы создаем Изолированный Сервис (Store):// shared-state.service.ts import { Injectable, signal } from '@angular/core'; @Injectable({ providedIn: 'root' // Глобальный синглтон, доступен везде автоматически! }) export class SharedStateService { globalLevel = signal(42); }Теперь любой компонент на любой глубине UI-дерева может получить доступ к этому состоянию одной строчкой, без каких-либо провайдеров в HTML:
// Глубоко вложенный дочерний компонент @Component({ selector: 'app-deep-child', standalone: true, template: `<button (click)="state.globalLevel.set(100)">Сбросить стейт из глубины</button>` }) export class DeepChild { // Настоящий аналог useContext() в Angular 20+: protected state = inject(SharedStateService); }Резюме ментального сдвига:
FormsModule— это просто утилита, которая избавляет вас от написания ручныхonChangeобработчиков для стандартных HTML инпутов.- Dependency Injection (DI) + функция inject() — это ваш новый инструмент вместо Context API, который позволяет связывать компоненты с глобальными или локальными сторами данных без раздувания HTML-шаблона километрами провайдеров.
🧪 Итоговый лабораторный вывод Дня 2
- Примитивы и Эффекты (
effect): ЗаменяютuseEffect, автоматически отслеживая только те сигналы, которые реально считываются внутри их контекста. Изменение неиспользуемых сигналов не вызывает повторных срабатываний. - Сигнальные инпуты (
input,input.required) и Outputs (output): Превращают входящие пропсы в безопасные readonly-сигналы, интегрируемые в computed-графы, а события передают наверх без лишних ререндеров и создания колбэков в памяти. - Глитч-фри архитектура: Движок Angular самостоятельно управляет графом зависимостей (DAG) в рантайме, избавляя разработчика от проблем с массивами зависимостей и
useCallback,useMemo.
