Привет, меня зовут Дарина. Два очень увлекательных года я работала в команде разработки дизайн-системы Prizm.
Это был мой первый опыт работы над «библиотечным» проектом, и он сильно расширил мой угол зрения и представления о том, как можно работать с кодом. Ещё он подарил множество интересных открытий, которые помогают мне в ежедневной работе и сейчас.
Завязка той истории, о которой я хочу рассказать, произошла почти шесть лет назад — точнее, в феврале 2020 года, когда команда Angular представила компилятор Ivy. По меркам разработки — это огромный срок. Хотя, если подумать, возможно, эта история началась ещё раньше — в момент создания документации для декоратора @Input().
Если в неё заглянуть, можно получить полную информацию об использовании: как создать alias, как применять трансформеры, есть и примеры работы с геттерами и сеттерами.
Но нас больше интересует то, чего там не написано, — а именно, когда же присваиваются значения входным параметрам.
Но обо всём по порядку.
Как всё началось
Однажды нам в техподдержку Prizm написали коллеги с запросом такого вида:
«Компонент Switcher не работает: не устанавливает активный элемент при инициализации приложения».
Компонент был несложный: набор кнопок, где можно выбрать активную. Внутри — два декоратора @Input(): список элементов и индекс активного. Проверили на демо-витрине — всё работает, а в проекте — нет.
Отработав все логичные версии и решительно отвергнув влияние магнитных бурь и ретроградного Меркурия, мы начали буквально посимвольно сравнивать реализацию.


И обнаружили следующее: разница была во входных параметрах, а точнее — в порядке их размещения: в шаблоне проекта сначала шёл selectedSwitcherIdx, потом switchers. А в документации — наоборот. Мы поменяли их местами в проекте, и компонент чудесным образом стал работать корректно.
Получалось так, что Angular учитывает порядок входных параметров в шаблоне и игнорирует то, в каком порядке свойства были объявлены в классе компонента.
При чём тут Ivy
Небольшое расследование показало, что такое поведение началось как раз с появлением Ivy.
Если вы помните времена ViewEngine, то тогда порядок зависел от того, как свойства объявлены в коде компонента, а не в шаблоне. Если один параметр зависел от другого, достаточно было объявить их в правильном порядке.
@Component({...})
export class Switcher {
@Input()
public switchers: SwitcherItem[] = []; // Объявляем свойство
@Input()
public set selectedSwitcherIdx(idx: number) {
const item = this.switchers[idx]; // Используем его, с ViewEngine все работает!
if (item) this.selectSwitcher(item, idx);
}
}
Такое поведение было удобно и интуитивно, но не зафиксировано в документации. Это позволило команде разработки Angular легко от него отказаться в пользу оптимизации сборки компонентов в новом движке и даже не включать этот момент в список breaking changes новой версии.
Поговаривают, что в списках ломающих изменений для внутреннего пользования этот пункт всё же был, но его сочли несущественным для публикации.
Конечно, я далеко не первый разработчик, столкнувшийся с этой проблемой. Обращения в GitHub Angular на эту тему до сих пор появляются с завидной регулярностью.
Официальный ответ команды на эти запросы выглядит так:
«Порядок установки атрибутов не является частью публичного API.
Если вам нужно контролировать порядок — используйте ngOnChanges».

Что видно на ревью
Казалось бы, прошло уже столько лет и все должны были к этому привыкнуть. Но на практике я до сих пор встречаю похожие проблемы во время проведения код-ревью. Особенно часто это случается в так называемых «глупых» — stateless — компонентах, где логика минимальна, но используется сеттер для реакции на изменение входных параметров.
Вот типичная картина:
несколько
@Input(), и один из них внутри сеттера обращается к другому (часто это даже не бросается в глаза, так как скрыто вызовом функции);порядок объявления в шаблоне никто не контролирует;
всё работает «случайно», пока кто-то не отформатирует шаблон.
Результат — непредсказуемое поведение и долгие поиски, почему «оно перестало работать, хотя я ничего не менял».
Поэтому на ревью я всегда обращаю внимание на сеттеры. Если их несколько и внутри происходит что-то кроме простого присваивания — значит, там есть место для сюрпризов.
Как жить с этой особенностью
Первый и самый очевидный вариант сегодня — использовать сигналы и их механизм computed. При полном сохранении поддержки декоратора @Input() разработчики Angular в новых проектах рекомендуют применять signal-based input функцию.
Это действительно удобнее и понятнее, чем связка «декоратор + сеттер». Красиво, лаконично и без лишних побочных эффектов — если использовать аккуратно. Нужно учитывать, что функции transform внутри сигналов тоже не гарантируют порядок, поэтому сложную логику лучше выносить в эффекты.
@Component({...})
export class Swithcher {
switchers = input<SwitcherItem[]>([]);
selectedSwitcherIdx = input<number>(0);
activeItem = computed(() => this.switchers()[this.selectedSwitcherIdx()]);
}
Если у нас более ранняя версия Angular или по какой-то другой причине мы не используем сигналы в проекте, то самый надёжный вариант — поступить так, как советуют авторы фреймворка: подписаться на событие жизненного цикла компонента.
Метод ngOnChanges вызывается после того, как Angular обновил все входные параметры, — все значения уже точно записаны, и можно спокойно работать с зависимыми данными.
ngOnChanges(changes: SimpleChanges): void {
if (changes['items'] || changes['activeIndex']) {
this.updateActive();
}
}
Всё предсказуемо, никаких влияний порядка в шаблоне.
Так же поможет и реактивный подход — он надёжно изолирует зависимости, хотя может быть чуть тяжелее по производительности.
private readonly items$ = new BehaviorSubject<SwitcherItem[]>([]);
private readonly activeIndex$ = new BehaviorSubject<number>(0);
@Input() set items(value: SwitcherItem[]) {
this.items$.next(value);
}
@Input() set activeIndex(value: number) {
this.activeIndex$.next(value);
}
readonly vm$ = combineLatest([this.items$, this.activeIndex$]);
Вместо заключения
Эта история научила меня быть осторожнее с использованием связки декоратора @Input() и сеттера, дала мне ещё один аргумент в пользу известного правила: не перегружать логикой сеттеры. И, пожалуй, это хороший повод ещё раз взглянуть на собственные компоненты: не слишком ли мы доверяем фреймворку, действительно ли мы контролируем то, что происходит в приложении? Сигналы во многом решили старые проблемы, но они не отменяют главного — аккуратного отношения к данным и зависимостям.
Если вы хотите подробнее узнать позицию команды разработки Angular в этом вопросе, ниже несколько ссылок, которые могут быть полезны: