# RxJS в Angular 2026: оператори, Subject'и та interop із Signals > RxJS в Angular 2026: опануйте оператори, Subject'и та патерни interop із Signals, які розробники використовують у продакшені, плюс найпоширеніші питання співбесід. - Published: 2026-07-02 - Updated: 2026-07-07 - Author: SharpSkill - Tags: angular, rxjs, signals, observables, typescript, deep-dive - Reading time: 10 min --- RxJS в Angular залишається основою асинхронного програмування у 2026 році, навіть попри те, що Signals змінюють спосіб, у який компоненти Angular відстежують синхронний стан. Уміння визначити, коли звернутися до Observable, коли краще підходить Signal і як ці два підходи взаємодіють, стало базовою навичкою для кожного розробника Angular. Цей докладний розбір проводить через оператори, Subject'и та патерни взаємодії, які відрізняють упевнену відповідь на співбесіді від тієї, що загрузла на місці. > **Signals проти Observables в одному реченні** > > Signals моделюють синхронний стан, який завжди має поточне значення; Observables моделюють асинхронні потоки подій, розподілені в часі. Angular 20 зберігає обидві моделі, а пакет `@angular/core/rxjs-interop` є офіційним мостом між ними. ## Чому RxJS досі важливий поряд з Angular Signals Поява Signals спонукала багато команд запитати, чи не відходить RxJS у минуле. Відповідь, через три мажорні версії, — ні. Signals чудово представляють стан, який має значення просто зараз: лічильник, поле форми, вибрану вкладку. Observables чудово моделюють події, які надходять у часі й можуть потребувати перетворення, скасування або поєднання: відповіді HTTP, повідомлення WebSocket, введення з клавіатури, події роутера та таймери. Власні API Angular роблять цей поділ видимим. `HttpClient` досі повертає Observables. Реактивні форми надають `valueChanges` та `statusChanges` як Observables. Події роутера течуть потоком через Observable. Усюди, де через один канал проходить кілька асинхронних подій, оператори RxJS залишаються найвиразнішим доступним інструментом. Офіційний [посібник з реактивності Angular](https://angular.dev/guide/signals/rxjs-interop) розглядає обидві моделі як взаємодоповнювальні, а не конкурентні, а [документація RxJS](https://rxjs.dev/guide/overview) лишається обов'язковим довідником для будь-якого нетривіального потоку даних. Практичне правило, що витримує code review: використовуйте Signals для локального стану компонента, який читається в шаблонах, та Observables для асинхронних конвеєрів, що потребують операторів. Шар взаємодії з'єднує обидві моделі, тож жодна з них не стає стіною. Оператори поєднання підкріплюють цю думку. Координація кількох асинхронних джерел, як-от злиття параметрів маршруту зі збереженими фільтрами користувача та живою стрічкою цін, — це саме те, для чого створювали `combineLatest`, `withLatestFrom` та `forkJoin`. Signals можуть виводити обчислені значення з інших сигналів, але не здатні виразити координацію на основі часу на кшталт «випусти лише після того, як усі три джерела видали щонайменше одне значення». Ця декларативна композиція в часі — та тривка причина, чому RxJS лишається в наборі інструментів. ## Ключові оператори RxJS, на які покладається кожен розробник Angular Оператори — це чисті функції, які приймають Observable і повертають новий. Класичний сценарій співбесіди — пошук під час набору тексту, бо він задіює чотири оператори одночасно: обмежує частоту введення, пропускає дублікати, скасовує застарілі запити та відновлюється після помилок. ```typescript // search.component.ts import { Component, inject } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { debounceTime, distinctUntilChanged, switchMap, catchError } from 'rxjs/operators'; import { of } from 'rxjs'; @Component({ selector: 'app-search', imports: [ReactiveFormsModule], template: ``, }) export class SearchComponent { private http = inject(HttpClient); query = new FormControl(''); results$ = this.query.valueChanges.pipe( debounceTime(300), // wait until typing pauses for 300ms distinctUntilChanged(), // ignore emissions that repeat the last value switchMap((term) => // cancel the in-flight request, start a fresh one this.http.get(`/api/search?q=${term}`).pipe( catchError(() => of([])) // recover from a failed request without killing the stream ) ) ); } ``` Тонка деталь, яку промацують інтерв'юери, — чому `switchMap` перебуває на зовнішньому рівні, тоді як `catchError` вкладений усередину нього. Розміщення `catchError` на внутрішньому Observable HTTP означає, що невдалий запит повертає порожній масив, а зовнішній потік продовжує текти. Перенесення `catchError` до зовнішнього конвеєра завершило б увесь пошук після першої помилки, зламавши кожне наступне натискання клавіші. Кожен із чотирьох операторів сплющення відповідає на своє питання про конкурентність, і вибір неправильного — одне з найпоширеніших джерел станів гонитви в застосунках Angular. > **Вибір оператора сплющення** > > Використовуйте `switchMap`, щоб скасувати попередній внутрішній запит і зберегти лише найновіший (пошук типу typeahead). Використовуйте `mergeMap`, щоб виконувати кожен внутрішній запит конкурентно (незалежні паралельні завантаження). Використовуйте `concatMap`, щоб ставити запити в сувору чергу (послідовні записи, які не мають перекриватися). Використовуйте `exhaustMap`, щоб ігнорувати нові введення, доки поточне не завершиться (блокування двічі натиснутої кнопки надсилання). Правильний вибір тут перетворює норовливу функцію на передбачувану. [Модуль операторів RxJS у SharpSkill](/technologies/angular/interview-questions/rxjs-operators) відпрацьовує ці відмінності на оцінюваних питаннях, які відтворюють саме ті стани гонитви, яким запобігає кожен оператор. ## Subject'и та мультикастинг: BehaviorSubject, ReplaySubject і Subject Звичайний Observable є unicast: кожен підписник запускає свіже виконання. Subject є водночас Observable та Observer, що робить його multicast. Ця властивість робить Subject'и стандартним способом побудови легкого сховища стану або шини подій усередині сервісу. Три варіанти різняться тим, що вони відтворюють запізнілим підписникам. Звичайний `Subject` не випускає нічого з минулого, тож підписник бачить лише значення, що надходять після його підписки. `BehaviorSubject` зберігає поточне значення й одразу його відтворює, тому й потребує початкового значення. `ReplaySubject` буферизує конфігуровану кількість минулих випусків і відтворює їх усі. ```typescript // cart.service.ts import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class CartService { // BehaviorSubject holds the latest value and replays it to new subscribers private readonly itemsSubject = new BehaviorSubject([]); // expose a read-only stream, never the writable Subject itself readonly items$ = this.itemsSubject.asObservable(); add(item: string): void { const current = this.itemsSubject.value; // synchronous access to the last value this.itemsSubject.next([...current, item]); // emit the next immutable state } } ``` Тут важать дві звички. По-перше, надання `asObservable()` замість самого Subject завадить зовнішньому коду викликати `next()` і змінювати стан з чорного ходу. По-друге, випуск нового посилання на масив замість зміни на місці тримає виявлення змін та перевірки рівності передбачуваними. Цей патерн передує Signals і досі добре працює для спільного, ін'єктовного стану, який споживає багато непов'язаних компонентів. Утім, `BehaviorSubject`, що використовується суто для зберігання єдиного поточного значення, яке читається в шаблонах, тепер часто краще виразити як записуваний Signal, що прибирає підписку та церемонію `asObservable()`. Subject лишається сильнішим вибором, коли сховище має ще й перетворювати, обмежувати частоту чи поєднувати свої випуски з іншими потоками, оскільки ці оператори не мають прямого відповідника серед Signals. Рішення між ними залежно від випадку використання, а не типове застосування одного всюди, — ознака вправності, яку інтерв'юери винагороджують. ## Взаємодія RxJS із signals через toSignal і toObservable Пакет взаємодії постачає дві провідні функції. `toSignal` перетворює Observable на Signal лише для читання, який шаблон може споживати безпосередньо, та автоматично керує життєвим циклом підписки. `toObservable` робить зворотне, перетворюючи Signal на Observable, щоб оператори RxJS могли обробляти його зміни. ```typescript // dashboard.component.ts import { Component, signal, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { toSignal, toObservable } from '@angular/core/rxjs-interop'; import { switchMap } from 'rxjs/operators'; @Component({ selector: 'app-dashboard', template: `{{ user()?.name }}` }) export class DashboardComponent { private http = inject(HttpClient); // a Signal drives the query; toObservable turns each change into a stream event readonly userId = signal(1); private readonly user$ = toObservable(this.userId).pipe( switchMap((id) => this.http.get<{ name: string }>(`/api/users/${id}`)) ); // toSignal subscribes immediately and unsubscribes on destroy, no manual teardown readonly user = toSignal(this.user$, { initialValue: null }); } ``` Опція `initialValue` заслуговує на увагу. Без неї `toSignal` повертає Signal, типізований як значення або `undefined`, бо ще нічого не було випущено. Передавання `initialValue: null` дає шаблону визначений початковий стан і чистіший тип. Для синхронних джерел, як-от `BehaviorSubject`, `requireSync: true` гарантує, що значення доступне при першому читанні, і повністю прибирає `undefined` з типу. Цей двонапрямний міст дозволяє кодовій базі впроваджувати Signals поступово, не переписуючи кожен наявний конвеєр Observable. Супутній [модуль співбесіди з Angular Signals](/technologies/angular/interview-questions/angular-signals) докладно розкриває модель реактивності, а [огляд можливостей Signals в Angular 18](/blog/angular/angular-18-signals-new-features) простежує, як розвивалися ці примітиви. ## Мостування Observables до сигнальних ресурсів через rxResource Angular 20 просуває `rxResource` як декларативний спосіб завантаження асинхронних даних з Observable у стан на основі сигналів. Він відстежує стани завантаження, помилки та розв'язання як сигнали, що прибирає більшість ручного обліку підписок, якого раніше вимагали виклики HTTP. ```typescript // products.component.ts import { Component, signal, inject } from '@angular/core'; import { rxResource } from '@angular/core/rxjs-interop'; import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-products', template: `` }) export class ProductsComponent { private http = inject(HttpClient); readonly category = signal('laptops'); // Angular 20 renamed the fields: request -> params and loader -> stream readonly products = rxResource({ params: () => ({ category: this.category() }), stream: ({ params }) => this.http.get(`/api/products?category=${params.category}`), }); // products.value(), products.isLoading() and products.error() are all signals } ``` Перейменування з `request`/`loader` в Angular 19 на `params`/`stream` в Angular 20 часто стає пасткою, тож згадка актуальних імен полів свідчить про сучасні знання на співбесіді. Коли сигнал `category` змінюється, `rxResource` скасовує попередній запит і запускає новий потік, безкоштовно даючи семантику `switchMap`. [Довідник API rxResource](https://v20.angular.dev/api/core/rxjs-interop/rxResource) документує повний життєвий цикл статусу. ## Уникнення витоків пам'яті за допомогою takeUntilDestroyed Будь-який ручний `subscribe()`, що переживає свій компонент, спричиняє витік пам'яті й може викликати колбеки на знищеному поданні. Історичним рішенням був патерн `takeUntil(this.destroy$)` із Subject, що завершується в `ngOnDestroy`. Angular замінив цей шаблонний код оператором `takeUntilDestroyed`, який прив'язує підписку до поточного контексту ін'єкції або явного `DestroyRef`. ```typescript // live-feed.component.ts import { Component, inject, DestroyRef } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { interval } from 'rxjs'; @Component({ selector: 'app-live-feed', template: `` }) export class LiveFeedComponent { private destroyRef = inject(DestroyRef); constructor() { interval(1000) // completes the subscription automatically when the component is destroyed .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((tick) => console.log('tick', tick)); } } ``` Викликаний усередині конструктора чи ініціалізатора поля, `takeUntilDestroyed()` не потребує аргументу, бо читає навколишній `DestroyRef`. Викликаний деінде, наприклад усередині методу, він потребує явно переданого `DestroyRef`, як показано вище. Перевага `toSignal` та `rxResource` повністю усуває ручні підписки, що є найчистішим способом обійти витоки; коли сира підписка справді потрібна, `takeUntilDestroyed` є правильним запобіжником. > **Пастка контексту ін'єкції** > > Виклик `takeUntilDestroyed()` без аргументу поза контекстом ін'єкції кидає помилку часу виконання. Підписка всередині обробника події, `setTimeout` чи колбека RxJS перебуває поза цим контекстом, тож `DestroyRef` треба захопити раніше (зазвичай як ін'єктоване поле) і передати явно. Забути про це — найчастіша причина помилки «takeUntilDestroyed() can only be used within an injection context». ## Питання співбесід про RxJS в Angular, варті відпрацювання Інтерв'юери зазвичай кружляють навколо тих самих тем з високою сигнальною цінністю. Уміння чітко на них відповісти відділяє кандидатів, які користувалися RxJS, від тих, хто лише читав про нього. - **Hot проти cold Observables.** Cold Observable запускає свого продюсера на кожну підписку, тож кожен підписник отримує незалежне виконання (свіжий запит HTTP). Hot Observable ділить одного продюсера між підписниками, що й створюють Subject'и та `share()`. - **switchMap проти mergeMap проти concatMap проти exhaustMap.** `switchMap` скасовує попередній внутрішній Observable (ідеальний для пошуку). `mergeMap` виконує всі внутрішні Observables конкурентно. `concatMap` ставить їх у чергу по порядку. `exhaustMap` ігнорує нові введення, доки одне виконується (ідеальний для запобігання подвійному надсиланню форм). - **Як запобігти витокам пам'яті.** Надавайте перевагу `toSignal`, `rxResource` або пайпу `async`, які всі відписуються автоматично; для ручних підписок використовуйте `takeUntilDestroyed`. - **Коли перетворювати Observable на Signal.** Звертайтеся до `toSignal`, коли шаблону потрібне значення синхронно, а джерело асинхронне, дозволяючи виявленню змін читати найновіше значення без пайпу `async`. Опрацювання оцінюваних версій цих питань під тиском часу — найшвидший спосіб їх засвоїти. [Хаб технології Angular](/technologies/angular) групує модулі RxJS, Signals та виявлення змін, які безпосередньо відповідають цим питанням. ## Висновок RxJS та Signals — партнери в сучасному стеку Angular, а не суперники. Висновки, варті того, щоб узяти їх у наступний проєкт чи на співбесіду: - Використовуйте Signals для синхронного стану компонента та Observables для асинхронних конвеєрів подій, що потребують операторів. - Звертайтеся до `switchMap`, щоб скасувати застарілу роботу, `exhaustMap`, щоб блокувати дублікати, і тримайте `catchError` на внутрішньому Observable, щоб зовнішній потік пережив збої. - Надавайте стан Subject через `asObservable()` та випускайте незмінні значення, щоб тримати виявлення змін передбачуваним. - З'єднуйте обидві моделі через `toSignal` і `toObservable`, передаючи `initialValue` або `requireSync` для чистих типів. - Завантажуйте асинхронні дані декларативно за допомогою `rxResource` в Angular 20, пам'ятаючи про імена полів `params` і `stream`. - Захищайте кожну решту ручну підписку через `takeUntilDestroyed`, щоб усунути витоки. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/angular/rxjs-angular-operators-subjects-signals-interop