Как реализовать межкомпонентную коммуникацию без Input/Output?
В Angular для межкомпонентной коммуникации без использования @Input() и @Output() можно применять несколько архитектурных подходов. Это особенно актуально при взаимодействии между компонентами, не связанными напрямую через иерархию DOM (например, sibling-компоненты или компоненты в разных модулях). Ключевые техники включают: общие сервисы с Subject/BehaviorSubject, сервисы-событийные шины, ViewChild, ContentChild, локальные инжекторы, глобальные хранилища и реактивные библиотеки (RxJS, Signals, NgRx и т.д.).
1. Сервис-посредник (Shared Service)
Один из самых распространённых способов — использование сервиса, который инжектируется в оба компонента и использует Subject, BehaviorSubject или ReplaySubject из RxJS.
Пример
@Injectable({ providedIn: 'root' })
export class CommunicationService {
private dataSubject = new BehaviorSubject<string>('initial');
data$ = this.dataSubject.asObservable();
setData(value: string) {
this.dataSubject.next(value);
}
}
В одном компоненте:
constructor(private comm: CommunicationService) {}
updateValue() {
this.comm.setData('New value');
}
В другом компоненте:
constructor(private comm: CommunicationService) {}
ngOnInit() {
this.comm.data$.subscribe(value => {
this.received = value;
});
}
Таким образом достигается реактивная передача данных без @Input/@Output.
2. EventBus (Custom Event System)
Если необходимо передавать события, а не только данные, можно реализовать глобальную событийную шину на основе Subject.
@Injectable({ providedIn: 'root' })
export class EventBusService {
private event$ = new Subject<{ name: string, payload?: any }>();
emit(eventName: string, payload?: any) {
this.event$.next({ name: eventName, payload });
}
on(eventName: string): Observable<any> {
return this.event$.pipe(
filter(e => e.name === eventName),
map(e => e.payload)
);
}
}
Пример использования:
// Component A
this.eventBus.emit('user-logged-in', userData);
// Component B
this.eventBus.on('user-logged-in').subscribe(user => {
this.user = user;
});
Это особенно удобно для loosely-coupled компонентов и глобальных событий.
3. ViewChild и родительский доступ к дочернему компоненту
Вместо Input/Output родитель может напрямую обращаться к дочернему компоненту.
@ViewChild(ChildComponent) childComp!: ChildComponent;
ngAfterViewInit() {
this.childComp.doSomething();
}
Такой подход не требует Output и позволяет родителю напрямую вызывать методы дочернего компонента или получать доступ к его свойствам.
4. ContentChild и доступ к проецируемому компоненту
Если используется ng-content, можно обращаться к вложенному компоненту через ContentChild.
@ContentChild(MyInnerComponent) projectedComp!: MyInnerComponent;
ngAfterContentInit() {
this.projectedComp.someMethod();
}
Это позволяет компоненту-контейнеру взаимодействовать с вложенным содержимым.
5. Service Locator через Injector
Можно использовать Injector или EnvironmentInjector, чтобы динамически получить доступ к сервисам или компонентам.
constructor(private injector: Injector) {}
ngOnInit() {
const service = this.injector.get(MyService);
service.someMethod();
}
Также можно инжектировать компонент в динамический компонент, созданный через ComponentFactoryResolver.
6. NgRx или Signal Store
В крупных приложениях удобнее использовать глобальное хранилище.
Пример (NgRx):
-
Компонент A: store.dispatch(login({ user }))
-
Компонент B: store.select(selectUser).subscribe(...)
Пример (Signal Store):
const user = signal<User | null>(null);
setUser(u: User) { user.set(u); }
Компоненты подписываются на user() и автоматически реагируют на изменения без Input/Output.
7. Dependency Injection на уровне компонента
Можно предоставить отдельный сервис на уровне компонента и инжектировать его в дочерние компоненты.
@Component({
providers: \[LocalService\]
})
export class ParentComponent {}
Теперь LocalService будет разделяться только между ParentComponent и его потомками.
8. LocalStorage / SessionStorage как буфер обмена
В некоторых случаях обмен данными может происходить через браузерное хранилище:
localStorage.setItem('theme', 'dark');
// другой компонент
const theme = localStorage.getItem('theme');
Дополнительно можно наблюдать window.addEventListener('storage', ...) для реактивных обновлений.
9. Router Navigation + NavigationExtras
При переходе между маршрутами можно передать данные через NavigationExtras:
this.router.navigate(\['/target'\], { state: { data: 'value' } });
В компоненте-получателе:
const data = this.router.getCurrentNavigation()?.extras.state?.\['data'\];
10. InjectionToken как глобальный доступ к данным
Можно создать глобальный InjectionToken:
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
@NgModule({
providers: \[{ provide: APP_CONFIG, useValue: { title: 'Demo' } }\]
})
Затем инжектировать в компоненты без Input:
constructor(@Inject(APP_CONFIG) private config: AppConfig) {}
11. Portals (CDK)
CDK Portals позволяют отобразить один компонент внутри другого без связи по DOM-иерархии.
const portal = new ComponentPortal(MyComponent);
this.portalOutlet.attachComponentPortal(portal);
Коммуникация происходит через инжектируемые сервисы или @Input() на сам портал.
12. Signals (Angular 17+)
Signals позволяют организовать централизованное реактивное состояние.
const counter = signal(0);
const double = computed(() => counter() \* 2);
Оба компонента могут использовать один и тот же signal, независимо от их иерархии.
Эти техники позволяют гибко управлять связями между Angular-компонентами в зависимости от архитектуры, размера и специфики проекта.