RxJS в Angular 2026: оператори, Subject'и та interop із Signals

RxJS в Angular 2026: опануйте оператори, Subject'и та патерни interop із Signals, які розробники використовують у продакшені, плюс найпоширеніші питання співбесід.

RxJS в Angular 2026 оператори, Subject'и та interop із Signals

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 розглядає обидві моделі як взаємодоповнювальні, а не конкурентні, а документація RxJS лишається обов'язковим довідником для будь-якого нетривіального потоку даних.

Практичне правило, що витримує code review: використовуйте Signals для локального стану компонента, який читається в шаблонах, та Observables для асинхронних конвеєрів, що потребують операторів. Шар взаємодії з'єднує обидві моделі, тож жодна з них не стає стіною.

Оператори поєднання підкріплюють цю думку. Координація кількох асинхронних джерел, як-от злиття параметрів маршруту зі збереженими фільтрами користувача та живою стрічкою цін, — це саме те, для чого створювали combineLatest, withLatestFrom та forkJoin. Signals можуть виводити обчислені значення з інших сигналів, але не здатні виразити координацію на основі часу на кшталт «випусти лише після того, як усі три джерела видали щонайменше одне значення». Ця декларативна композиція в часі — та тривка причина, чому RxJS лишається в наборі інструментів.

Ключові оператори RxJS, на які покладається кожен розробник Angular

Оператори — це чисті функції, які приймають Observable і повертають новий. Класичний сценарій співбесіди — пошук під час набору тексту, бо він задіює чотири оператори одночасно: обмежує частоту введення, пропускає дублікати, скасовує застарілі запити та відновлюється після помилок.

search.component.tstypescript
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: `<input [formControl]="query" placeholder="Search repositories" />`,
})
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 відпрацьовує ці відмінності на оцінюваних питаннях, які відтворюють саме ті стани гонитви, яким запобігає кожен оператор.

Subject'и та мультикастинг: BehaviorSubject, ReplaySubject і Subject

Звичайний Observable є unicast: кожен підписник запускає свіже виконання. Subject є водночас Observable та Observer, що робить його multicast. Ця властивість робить Subject'и стандартним способом побудови легкого сховища стану або шини подій усередині сервісу.

Три варіанти різняться тим, що вони відтворюють запізнілим підписникам. Звичайний Subject не випускає нічого з минулого, тож підписник бачить лише значення, що надходять після його підписки. BehaviorSubject зберігає поточне значення й одразу його відтворює, тому й потребує початкового значення. ReplaySubject буферизує конфігуровану кількість минулих випусків і відтворює їх усі.

cart.service.tstypescript
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<string[]>([]);

  // 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 могли обробляти його зміни.

dashboard.component.tstypescript
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 докладно розкриває модель реактивності, а огляд можливостей Signals в Angular 18 простежує, як розвивалися ці примітиви.

Готовий до співбесід з Angular?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Мостування Observables до сигнальних ресурсів через rxResource

Angular 20 просуває rxResource як декларативний спосіб завантаження асинхронних даних з Observable у стан на основі сигналів. Він відстежує стани завантаження, помилки та розв'язання як сигнали, що прибирає більшість ручного обліку підписок, якого раніше вимагали виклики HTTP.

products.component.tstypescript
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<Product[]>(`/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 документує повний життєвий цикл статусу.

Уникнення витоків пам'яті за допомогою takeUntilDestroyed

Будь-який ручний subscribe(), що переживає свій компонент, спричиняє витік пам'яті й може викликати колбеки на знищеному поданні. Історичним рішенням був патерн takeUntil(this.destroy$) із Subject, що завершується в ngOnDestroy. Angular замінив цей шаблонний код оператором takeUntilDestroyed, який прив'язує підписку до поточного контексту ін'єкції або явного DestroyRef.

live-feed.component.tstypescript
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 групує модулі RxJS, Signals та виявлення змін, які безпосередньо відповідають цим питанням.

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Висновок

RxJS та Signals — партнери в сучасному стеку Angular, а не суперники. Висновки, варті того, щоб узяти їх у наступний проєкт чи на співбесіду:

  • Використовуйте Signals для синхронного стану компонента та Observables для асинхронних конвеєрів подій, що потребують операторів.
  • Звертайтеся до switchMap, щоб скасувати застарілу роботу, exhaustMap, щоб блокувати дублікати, і тримайте catchError на внутрішньому Observable, щоб зовнішній потік пережив збої.
  • Надавайте стан Subject через asObservable() та випускайте незмінні значення, щоб тримати виявлення змін передбачуваним.
  • З'єднуйте обидві моделі через toSignal і toObservable, передаючи initialValue або requireSync для чистих типів.
  • Завантажуйте асинхронні дані декларативно за допомогою rxResource в Angular 20, пам'ятаючи про імена полів params і stream.
  • Захищайте кожну решту ручну підписку через takeUntilDestroyed, щоб усунути витоки.

Теги

#angular
#rxjs
#signals
#observables
#typescript
#deep-dive

Поділитися

Пов'язані статті

NgRx Signal Store vs Classic NgRx порівняння

NgRx Signal Store vs Класичний NgRx у 2026: Який Обрати?

Комплексне порівняння NgRx Signal Store та класичного NgRx для управління станом в Angular. Практичні приклади коду та рекомендації на 2026 рік.

Оптимізація продуктивності Angular defer loading і lazy loading

Angular @defer у 2026: декларативне ледаче завантаження для швидших застосунків

Опануйте блоки @defer в Angular для декларативного ледачого завантаження. Глибокий розбір тригерів, prefetching, інкрементальної гідратації, поведінки в SSR і практичних патернів продуктивності.

Angular 20 Resource API та httpResource: посібник з прикладами коду та питаннями для співбесід

Angular 20 Resource API, httpResource та питання для технічних співбесід

Детальний посібник з Resource API в Angular 20: resource(), rxResource() та httpResource() для реактивного отримання даних. Валідація Zod, рядкові статуси, міграція з HttpClient та актуальні питання для технічних співбесід Angular у 2026 році.