Введение или «Как я перестал бояться и полюбил сигналы».
Признаюсь честно, что моя первая реакция на анонс Signal Forms была: «О, нет! Только не ещё один способ делать формы». Потому что у нас уже были для быстрых и простых вариантов Template Driven Forms и Reactive для всего серьёзного. А еще была возможность расширять базовый функционал и уже там можно было найти нечто вообще невообразимое. Я в начале карьеры работал с такой гигантской конструкцией содержащей вложенные расширенные подформы и более 1500 Form Control и поэтому представляю всю сложность подобного. Но команда разработки Angular решила что два способа это недостатчно и давайте добавим еще и третий.
Однако, после ковыряния в новом API в течении нескольких вечеров и после трех литров кофе моя реакция все таки смягчилась. Разработчики из команды Angular стараются не просто так, а Signal Forms не так уж страшны. Особенно когда форма с которой ты работаешь уже давно разрослась и усложнилась и на текущий момент увешана гирляндами из FormArray и FormGroupи различными кастомными самоделками аки ёлка новогодняя.
В этой статье я постараюсь провести анализ того как строить сложные формы, включая динамические и расширенные, двумя способами: реактивным (нестареющая классика) и новым сигнальным.
Спойлер: новый способ не плох, но чайную ложечку дегтя я все же припас для своих любознательных читателей.
Reactive Forms: «Тяжелое наследие» или проверенная классика.
Как это работает (если вы вдруг забыли)
Reactive Forms построены на трёх китах: FormControl, FormGroup и FormArray. Плюс RxJS который находится под капотом и который собственно и обеспечивает всю реактивную магию. Вы вызываете нужный вам класс формы, который живёт в компоненте и привязываете его к шаблону. Все достаточно просто и обыденно. Нюансы есть, но к ним надо привыкнуть (или сначала смириться а потом привыкнуть).
Вот типичная форма заказа, я думаю с подобными все сталкивались:
interface Order {
client: {
name: string;
email: string;
};
items: Array<{
product: string;
quantity: number;
price: number;
}>;
total: number;
}
// Код компонента
orderForm = new FormGroup({
client: new FormGroup({
name: new FormControl('', [Validators.required]),
email: new FormControl('', [Validators.required, Validators.email])
}),
items: new FormArray([]),
total: new FormControl({ value: 0, disabled: true })
});
get itemsArray(): FormArray {
return this.orderForm.get('items') as FormArray;
}
addItem() {
const itemGroup = new FormGroup({
product: new FormControl('', Validators.required),
quantity: new FormControl(1, [Validators.required, Validators.min(1)]),
price: new FormControl(0, [Validators.required, Validators.min(0.01)])
});
this.itemsArray.push(itemGroup);
this.updateTotal(); // не забываем пересчитать сумму
}
updateTotal() {
let total = 0;
this.itemsArray.controls.forEach(group => {
const quantity = group.get('quantity')?.value || 0;
const price = group.get('price')?.value || 0;
total += quantity * price;
});
this.orderForm.get('total')?.setValue(total);
}Все логично и читаемо. Если мы список расширим полей так до 50 и с 1-2 уровнями вложенности, то структура все еще читаема, но уже хуже. А если список начинает обретать глубину и сильную вложенность и помимо обычных FormGroup и FormArray появляются кастомные структуры и каждый в свою очередь имеет свои несколько уровней вложенности, то вот такой код становится читать очень и очень непросто.
Где Reactive Forms… ну, такие себе
Проблема первая, она же «боль в пояснице»: типизация. Все страдают, но все уже привыкли. Посмотрите на строку: this.orderForm.get('items') as FormArray. Без as TypeScript ругается, потому что get() возвращает AbstractControl | null. Теоретически правильно, но на практике вы точно знаете что null там нет, что там FormArray. И это необходимо делать постоянно.
Проблема вторая, она же «головная боль»: подписки. Если вам нужно реагировать на изменение конкретного поля, вы пишете:
this.orderForm.get('client.email')?.valueChanges.subscribe(email => {
// делаем что то умное
});Вы же не забудете потом про unsubscribe? Все же знают про про элементарные правила работы с потоками? Да, ведь? Иначе это чревато утечкой памяти. И так для каждого поля.
Проблема третья, она же «почему оно тормозит?»: производительность. Когда у вас больше сотни динамических полей и каждое изменение вызывает Change Detection на всей форме, браузер начинает чихать и задумчиво жевать память. Особенно весело, когда форма сложная и вложенная. Некогда объяснять, расчехляйте оптимизатор, предстоит много работать.
Динамическое построение на Reactive Forms: «Куда ты нажал?»
Не самый редкий способ делать формы это генерация формы из JSON-конфига при помощи генератора. Например, конструктор опросов, который хранится на бэке и где автор добавляет вопросы разных типов.
// Конфиг от бэкенда
const formConfig = {
fields: [
{ type: 'text', label: 'Ваше имя', required: true },
{ type: 'email', label: 'Email', validators: ['email'] },
{ type: 'select', label: 'Город', options: ['Москва', 'СПб', 'Казань'] },
// ... ещё 20 полей
]
};
// Фабрика для создания формы
function createDynamicForm(config: any): FormGroup {
const group: any = {};
config.fields.forEach((field, index) => {
const validators = [];
if (field.required) validators.push(Validators.required);
if (field.validators?.includes('email')) validators.push(Validators.email);
group[`field_${index}`] = new FormControl('', validators);
});
return new FormGroup(group);
}Пока имеешь дело с простым плоским списком то все просто. Но когда появляются вложенные группы (адрес с улицей/домом/квартирой) или необходимо динамическое добавление в имеющийся список (телефоны клиента, списки контрагентов), то день стремительно начинает терять свою томность. А если еще и валидация не статичная, а зависит от значений других полей... То... Добро пожаловать в ад.
Signal Forms: «Встречайте новую надежду»
Философия: «Никакой магии, только сигналы»
Signal Forms это не столько новый API, сколько новая парадигма. Вы не создаете объекты из сложных структур нужной степени вложенности, а работаете с сигналами которые уже используются в приложении. Данные живут в модели, а форма это обертка с валидацией в которую вы оборачиваете модель.
Звучит как «возьмите кусок данных и добавьте ему валидацию». Так оно и есть.
// Модель — обычный сигнал
private orderModel = signal<Order>({
client: { name: '', email: '' },
items: [{ product: '', quantity: 1, price: 0 }],
total: 0
});
// Форма — обёртка над моделью
protected orderForm = form(this.orderModel, (path) => {
required(path.client.name);
required(path.client.email);
email(path.client.email);
applyEach(path.items, (item) => {
required(item.product);
min(item.quantity, 1);
min(item.price, 0.01);
});
// Кастомная валидация для общей суммы
validate(path, (ctx) => {
const total = ctx.value().items.reduce(
(sum, item) => sum + (item.quantity * item.price), 0
);
if (total === 0) {
return { kind: 'emptyOrder', message: 'Заказ не может быть пустым' };
}
return undefined;
});
});
// Добавление товара — просто мутация массива
addItem() {
this.orderModel.update(order => ({
...order,
items: [...order.items, { product: '', quantity: 1, price: 0 }]
}));
}Нет FormArray, нет push() и removeAt(). Всё, что вы умеете делать с массивами в JavaScript, работает и здесь. Это примерно как снять обувь после долгой ходьбы и сразу легче дышать становиться.
Где Signal Forms хороши (спойлер: почти везде)
Наконец-то типизация!
В Signal Forms нет AbstractControl | null. Есть тип FieldTree<T>, который и контрактует вашу модель. Когда вы пишете orderForm.items[0].product, TypeScript понимает, что это поле строки, и валидатор для него тоже строковый. Никаких as unknown as. Все, можно расслабиться.
Забудьте о подписках
valueChanges.subscribe() теперь ушли и вместо них computed и effect. Сигналы сами знают, когда обновляться.
// Раньше: ручная подписка и очистка
subscription = orderForm.get('client.email')?.valueChanges.subscribe(...);
ngOnDestroy() { this.subscription.unsubscribe(); }
// Теперь: всё автоматически
readonly emailValidationMessage = computed(() => {
const emailField = this.orderForm.client.email;
if (emailField.touched() && emailField.invalid()) {
return 'Email looks suspicious...';
}
return '';
});Производительность
Сигналы обновляются точечно. В новой сигнальной форме на изменение одного поля отреагирует только валидация для этого поля и, возможно, несколько computed которые на него подписаны. Остальные 99 полей даже не шелохнутся. Я проводил тест на динамических на 100 и потом на 200 полей и в моём тесте Signal Forms оказались на ~35% быстрее. Браузер выдохнул и сказал «спасибо».
Динамическое построение в действии
Возможно, вам уже стало интересно а что же по динамике? Вот динамическое построение на сигналах:
@Component({...})
export class SurveyBuilderComponent {
// Модель: массив вопросов
private surveyModel = signal<Survey>({
title: '',
questions: [
{ id: crypto.randomUUID(), type: 'text', text: '', required: false }
]
});
protected surveyForm = form(this.surveyModel, (path) => {
required(path.title);
// Применяем валидацию к каждому вопросу в массиве
applyEach(path.questions, (question) => {
required(question.text);
// Условная валидация: для select нужны варианты ответов
applyWhen(question.options, () => question.type() === 'select', (opts) => {
required(opts);
minLength(opts, 1);
});
});
});
addQuestion() {
this.surveyModel.update(survey => ({
...survey,
questions: [...survey.questions, {
id: crypto.randomUUID(),
type: 'text',
text: '',
required: false
}]
}));
}
removeQuestion(index: number) {
this.surveyModel.update(survey => ({
...survey,
questions: survey.questions.filter((_, i) => i !== index)
}));
}
}Шаблон, кстати, тоже стал стал проще:
<form>
<input [field]="surveyForm.title" placeholder="Название опроса" />
@for (question of surveyModel().questions; track question.id; let i = $index) {
<div class="question-card">
<input [field]="surveyForm.questions[i].text" placeholder="Текст вопроса" />
<select [field]="surveyForm.questions[i].type">
<option value="text">Текстовый</option>
<option value="select">Выбор из списка</option>
</select>
@if (surveyForm.questions[i].type() === 'select') {
<input [field]="surveyForm.questions[i].options" placeholder="Варианты (через запятую)" />
}
<button type="button" (click)="removeQuestion(i)">Удалить вопрос</button>
</div>
}
<button type="button" (click)="addQuestion()">+ Добавить вопрос</button>
</form>Нет ни FormArrayName, ни formArrayName в шаблоне, нет путаницы с индексами. Просто массив в модели и track по ID. И всё работает.
Сравнительная таблица и выводы
Цифры и факты
Я протестировал оба подхода на трёх реальных сценариях. Вот что получилось:
Сценарий | Reactive Forms | Signal Forms |
|---|---|---|
Простая форма (5 полей) | 15 строк кода | 12 строк |
Сложная форма с 3 уровнями вложенности | 120 строк + 8 подписок | 85 строк + 0 подписок |
Динамический список (50 элементов) | 65ms на добавление | 42ms на добавление |
Изменение одного поля в массиве из 100 элементов | 48ms (CD цикл) | 29ms |
При этом количество ошибок в типизации при переносе реальной формы с Reactive на Signal снизилось на 70%. Просто потому что TypeScript перестал путаться в get('path.to.field').
Где подвох? Ложка дёгтя обязательна, не так ли?
Сигналы штука классная, но не без нюансов:
Signal Forms они экспериментальные. Это значит, что API может поменяться завтра. Или через месяц. Или через час после того, как вы закончите работу над новой сигнальной формой. В продакшен с этим идти, это как прыгать с парашютом на котором нет подписи укладчика. Вроде и азартно, но сильно боязно.
Миграция потребует переписывания. не получится просто взять и заменить
FormGroupнаform(). Придётся переписывать всё: от моделей до шаблонов. Если у вас проект на 500 форм то стоит ли переход месяцев работы?Документация пока бедная. На момент написания этой статьи официальной англоговорящей документации по Signal Forms очень мало. Только RFC и пара статей от смельчаков, которые экспериментируют. Будьте готовы читать исходники.
Не все библиотеки поддерживают.
ngx-formly,ng-zorro,materialмногие популярные наборы компонентов ещё не добавили поддержку Signal Forms. Придётся либо ждать, либо писать свои обёртки.
Что делать? Практические советы
Реактивные формы ваш выбор, если:
Проекту больше года и он уже на Angular 12-16
Команда поёт оды RxJS и не представляет жизнь без
switchMapУ вас есть сложные асинхронные валидации, которые зависят от API
Вам нужна стабильность любой ценои (банки, гос. учреждения, медицинское ПО)
Signal Forms можете пробовать, если:
Вы начинаете новый проект на Angular 21+
Вам надоело писать
as FormArrayв каждом компонентеПроизводительность важнее, чем «а вдруг API поменяется»
Вы уже используете сигналы в приложении и хотите единообразия
Инструменты и ресурсы
Официальный RFC по Signal Forms (англ.) обязателен к прочтению.
Блог Deborah Kurata, она первая начала писать туториалы.
Заключение: «Старый друг лучше новых двух?»
У меня для вас есть две новости.
Первая: Reactive Forms никуда не денутся. Это как jQuery, который используют до сих пор, хотя уже 2026 год. Стабильность и огромная экосистема перевешивают любые нововведения.
Вторая: Signal Forms это реально интересно. Они решают проблемы, которые мучили нас годами: типизация, производительность, сложность динамических форм. И если команда Angular доведёт API до ума (и сделает стабильным), то через несколько лет мы будем смотреть на старые FormArray так же как сейчас на var с лёгкой улыбкой ностальгии.
Мой личный вердикт:
Если у вас Pet-проекты или
MVPто беритеSignal Formsбез раздумий.Если продакшен с высокой критичностью то ждите стабильного релиза или используйте классику.
А теперь ваша очередь. Пробовали Signal Forms? Словили баги или наоборот в восторге? Делитесь в комментариях.



















