RxJS dla Angular Developer - Kompletny Przewodnik Rekrutacyjny 2026

Sławomir Plamowski 12 min czytania
angular frontend observable programowanie-reaktywne pytania-rekrutacyjne rxjs

Jeśli aplikujesz na stanowisko Angular Developer, RxJS nie jest opcjonalny - to absolutny fundament. Angular opiera cały swój ekosystem na reaktywności: HTTP Client zwraca Observable, Router emituje eventy jako strumienie, Reactive Forms to Observable pod maską. Kandydaci, którzy nie rozumieją RxJS, odpadają na pierwszych pytaniach technicznych.

W tym przewodniku przejdziemy przez wszystko, co musisz wiedzieć o RxJS na rozmowę Angular - od podstaw Observable, przez kluczowe operatory, po zaawansowane wzorce zarządzania stanem. Każda sekcja zawiera konkretne pytania z odpowiedziami, które usłyszysz na rozmowie.

Dlaczego RxJS jest krytyczny dla Angular

Zanim zagłębimy się w szczegóły, musisz zrozumieć dlaczego RxJS i Angular są nierozłączne:

Obszar Angular Użycie RxJS
HttpClient Wszystkie requesty zwracają Observable
Router Events, params, queryParams jako Observable
Reactive Forms valueChanges, statusChanges
@Output() EventEmitter to Subject
AsyncPipe Natywna obsługa Observable w szablonach
State Management NgRx, NGXS oparte na RxJS

Jeśli przyjdziesz na rozmowę Angular mówiąc "znam trochę RxJS", to jak powiedzenie "znam trochę JavaScript" na rozmowę frontend. RxJS to nie dodatek - to sposób myślenia o danych w Angular.

Observable - Podstawa wszystkiego

Odpowiedź w 30 sekund

Kiedy rekruter zapyta "Czym jest Observable?":

Observable to strumień danych, który może emitować wiele wartości w czasie. W przeciwieństwie do Promise, który zwraca jedną wartość i kończy, Observable może emitować ciągle - jak zdarzenia użytkownika czy WebSocket. Observable jest leniwy - nie wykonuje się dopóki ktoś nie zasubskrybuje. I co kluczowe - można go anulować przez unsubscribe, co jest fundamentalne dla zarządzania pamięcią w Angular.

Observable vs Promise - co odpowiedzieć

To pytanie pada na każdej rozmowie Angular. Przygotuj się na tabelę:

Cecha Observable Promise
Wartości 0, 1 lub wiele Dokładnie 1
Ewaluacja Lazy (po subscribe) Eager (od razu)
Anulowanie Tak (unsubscribe) Nie
Operatory 100+ operatorów RxJS then/catch/finally
W Angular HttpClient, Router, Forms Rzadko używane
// Observable - może emitować wiele wartości, można anulować
import { interval } from 'rxjs';
import { take } from 'rxjs/operators';

const observable$ = interval(1000).pipe(take(5));

const subscription = observable$.subscribe(
  value => console.log('Tick:', value)
);

// Anuluj po 2.5 sekundach - zostanie tylko 0, 1, 2
setTimeout(() => subscription.unsubscribe(), 2500);

// Promise - jedna wartość, nie można anulować
const promise = fetch('/api/users');
// promise się wykona niezależnie od tego czy używasz wyniku

Rekruterzy często pytają: "Kiedy użyłbyś Promise zamiast Observable w Angular?"

Odpowiedź: Prawie nigdy. Angular HttpClient zwraca Observable. Jedyny przypadek to integracja z zewnętrznymi bibliotekami bazującymi na Promise (np. stare SDK). Wtedy używasz from(promise) do konwersji na Observable.

Operatory - serce RxJS

Znajomość operatorów odróżnia początkującego od doświadczonego Angular developera. Nie musisz znać wszystkich 100+, ale te poniżej to absolutne minimum.

map, filter, tap - podstawy

import { of } from 'rxjs';
import { map, filter, tap } from 'rxjs/operators';

// Dane użytkowników z API
of({ id: 1, name: 'Jan', age: 25 }, { id: 2, name: 'Anna', age: 17 })
  .pipe(
    tap(user => console.log('Przed filtrem:', user)), // Debug bez modyfikacji
    filter(user => user.age >= 18),                   // Tylko pełnoletni
    map(user => user.name.toUpperCase())              // Transformuj na wielkie litery
  )
  .subscribe(name => console.log('Wynik:', name));
// Output: Wynik: JAN

Rekruter może zapytać: "Jaka jest różnica między map a tap?"

map transformuje wartość - zwraca nową wartość która idzie dalej w strumieniu. tap to side effect - wykonuje operację (logging, debug) ale nie zmienia wartości. Zawsze zwraca to co dostał.

switchMap vs mergeMap vs concatMap

To jedno z najczęstszych pytań na rozmowach Angular. Każdy z tych operatorów mapuje wartość na nowy Observable, ale różnią się zachowaniem:

import { fromEvent, interval } from 'rxjs';
import { switchMap, mergeMap, concatMap, take } from 'rxjs/operators';

const clicks$ = fromEvent(document, 'click');

// SWITCHMAP - anuluje poprzednią subskrypcję
// Idealne do: wyszukiwania, HTTP gdzie liczy się ostatni wynik
clicks$.pipe(
  switchMap(() => interval(1000).pipe(take(3)))
).subscribe(x => console.log('switchMap:', x));
// Kliknięcie anuluje poprzedni interval i zaczyna nowy

// MERGEMAP - równoległe wykonanie, bez anulowania
// Idealne do: zapisywania wielu elementów, operacji gdzie potrzebujesz wszystkich wyników
clicks$.pipe(
  mergeMap(() => interval(1000).pipe(take(3)))
).subscribe(x => console.log('mergeMap:', x));
// Każde kliknięcie dodaje nowy interval, wszystkie działają równolegle

// CONCATMAP - kolejka, jeden po drugim
// Idealne do: operacji które muszą być sekwencyjne
clicks$.pipe(
  concatMap(() => interval(1000).pipe(take(3)))
).subscribe(x => console.log('concatMap:', x));
// Czeka aż poprzedni interval się skończy zanim zacznie następny

Praktyczna reguła: W 90% przypadków w Angular używasz switchMap - dla HTTP requests, wyszukiwania, nawigacji. mergeMap gdy naprawdę potrzebujesz równoległości. concatMap gdy kolejność jest krytyczna.

debounceTime i distinctUntilChanged

Ten duet pojawia się w każdej aplikacji Angular z wyszukiwaniem:

import { fromEvent } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';

@Component({
  template: `<input #searchInput type="text" placeholder="Szukaj...">`
})
export class SearchComponent implements AfterViewInit {
  @ViewChild('searchInput') searchInput: ElementRef;

  ngAfterViewInit() {
    fromEvent(this.searchInput.nativeElement, 'input').pipe(
      map((event: Event) => (event.target as HTMLInputElement).value),
      debounceTime(300),           // Czekaj 300ms po ostatnim znaku
      distinctUntilChanged(),       // Ignoruj jeśli wartość się nie zmieniła
      switchMap(term => this.searchService.search(term))
    ).subscribe(results => {
      this.results = results;
    });
  }
}

Rekruter może zapytać: "Po co distinctUntilChanged?"

Wyobraź sobie: użytkownik wpisuje "ang", potem usuwa "g" i znów wpisuje "g". Wartość wraca do "ang". Bez distinctUntilChanged wysłałbyś dwa identyczne requesty. Z nim - tylko jeden.

Subjects - emitowanie wartości

Subjects to Observable które możesz kontrolować z zewnątrz - ręcznie emitować wartości. W Angular są kluczowe do komunikacji między komponentami i zarządzania stanem.

Rodzaje Subjects

import { Subject, BehaviorSubject, ReplaySubject, AsyncSubject } from 'rxjs';

// SUBJECT - podstawowy, nie ma wartości początkowej
const subject = new Subject<number>();
subject.subscribe(x => console.log('Sub A:', x));
subject.next(1); // Sub A: 1
subject.next(2); // Sub A: 2

// BEHAVIORSUBJECT - przechowuje ostatnią wartość
// Idealne do: current user, app settings, selected item
const behavior = new BehaviorSubject<string>('początkowa wartość');
behavior.subscribe(x => console.log('Behavior A:', x)); // Od razu: początkowa wartość
behavior.next('nowa wartość');
behavior.subscribe(x => console.log('Behavior B:', x)); // Od razu: nowa wartość

// REPLAYSUBJECT - przechowuje N ostatnich wartości
// Idealne do: cache, historia akcji
const replay = new ReplaySubject<number>(3); // Pamiętaj 3 ostatnie
replay.next(1);
replay.next(2);
replay.next(3);
replay.next(4);
replay.subscribe(x => console.log('Replay:', x)); // Od razu: 2, 3, 4

// ASYNCSUBJECT - emituje tylko ostatnią wartość i tylko po complete
const async = new AsyncSubject<string>();
async.next('a');
async.next('b');
async.next('c');
async.subscribe(x => console.log('Async:', x)); // Nic jeszcze...
async.complete(); // Teraz: c

BehaviorSubject w serwisach Angular

To najpopularniejszy pattern do state management w Angular:

@Injectable({ providedIn: 'root' })
export class UserService {
  // Private BehaviorSubject - nie można modyfikować z zewnątrz
  private currentUserSubject = new BehaviorSubject<User | null>(null);

  // Public Observable - komponenty mogą subskrybować
  currentUser$ = this.currentUserSubject.asObservable();

  // Getter dla synchronicznego dostępu
  get currentUser(): User | null {
    return this.currentUserSubject.getValue();
  }

  login(credentials: Credentials): Observable<User> {
    return this.http.post<User>('/api/login', credentials).pipe(
      tap(user => this.currentUserSubject.next(user))
    );
  }

  logout(): void {
    this.currentUserSubject.next(null);
  }
}

// W komponencie
@Component({
  template: `
    <div *ngIf="currentUser$ | async as user">
      Zalogowany jako: {{ user.name }}
    </div>
  `
})
export class NavbarComponent {
  currentUser$ = this.userService.currentUser$;

  constructor(private userService: UserService) {}
}

Rekruter może zapytać: "Dlaczego asObservable()?"

Enkapsulacja. Nie chcesz, żeby komponenty mogły robić userService.currentUser$.next(...). Tylko serwis kontroluje kiedy i jak zmienia się stan. To podstawowa zasada dobrej architektury.

HTTP i RxJS w Angular

HttpClient Angular zwraca Observable - musisz rozumieć jak to obsługiwać.

Podstawowe żądania

@Injectable({ providedIn: 'root' })
export class ApiService {
  constructor(private http: HttpClient) {}

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users');
  }

  createUser(user: CreateUserDto): Observable<User> {
    return this.http.post<User>('/api/users', user);
  }

  updateUser(id: number, data: Partial<User>): Observable<User> {
    return this.http.put<User>(`/api/users/${id}`, data);
  }

  deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`/api/users/${id}`);
  }
}

Obsługa błędów

import { catchError, retry, retryWhen, delay, take } from 'rxjs/operators';
import { throwError, of, timer } from 'rxjs';

// Podstawowa obsługa błędów
getUsers(): Observable<User[]> {
  return this.http.get<User[]>('/api/users').pipe(
    catchError(error => {
      console.error('Błąd pobierania użytkowników:', error);
      // Opcja 1: Zwróć pustą tablicę jako fallback
      return of([]);
      // Opcja 2: Przekaż błąd dalej z lepszym opisem
      // return throwError(() => new Error('Nie udało się pobrać użytkowników'));
    })
  );
}

// Retry z exponential backoff
getUsersWithRetry(): Observable<User[]> {
  return this.http.get<User[]>('/api/users').pipe(
    retry({
      count: 3,
      delay: (error, retryCount) => {
        console.log(`Retry ${retryCount}...`);
        return timer(Math.pow(2, retryCount) * 1000); // 2s, 4s, 8s
      }
    }),
    catchError(error => {
      console.error('Błąd po 3 próbach:', error);
      return of([]);
    })
  );
}

Łączenie wielu żądań

import { forkJoin, combineLatest } from 'rxjs';
import { switchMap, map } from 'rxjs/operators';

// FORKJOIN - czeka na wszystkie i zwraca razem
// Używaj gdy: potrzebujesz wszystkich danych jednocześnie, żadne nie zmienia się w czasie
loadDashboardData(): Observable<DashboardData> {
  return forkJoin({
    users: this.http.get<User[]>('/api/users'),
    posts: this.http.get<Post[]>('/api/posts'),
    stats: this.http.get<Stats>('/api/stats')
  });
}

// COMBINELATEST - reaguje na każdą zmianę
// Używaj gdy: dane mogą się zmieniać (np. z WebSocket)
liveData$ = combineLatest({
  users: this.usersService.users$,
  filters: this.filtersService.filters$
}).pipe(
  map(({ users, filters }) => this.applyFilters(users, filters))
);

// Sekwencyjne żądania - jedno zależy od drugiego
getUserWithPosts(userId: number): Observable<UserWithPosts> {
  return this.http.get<User>(`/api/users/${userId}`).pipe(
    switchMap(user =>
      this.http.get<Post[]>(`/api/users/${userId}/posts`).pipe(
        map(posts => ({ ...user, posts }))
      )
    )
  );
}

Zarządzanie subskrypcjami

Wycieki pamięci to najpowszechniejszy błąd z RxJS w Angular. Rekruterzy zawsze pytają jak im zapobiegać.

Async Pipe - preferowane rozwiązanie

@Component({
  template: `
    <!-- Async pipe automatycznie subskrybuje i unsubscribe -->
    <ul>
      <li *ngFor="let user of users$ | async">{{ user.name }}</li>
    </ul>

    <!-- Z ngIf dla null check -->
    <div *ngIf="currentUser$ | async as user">
      Witaj, {{ user.name }}!
    </div>

    <!-- Wiele Observable - unikaj wielu async pipe -->
    <ng-container *ngIf="{ users: users$ | async, loading: loading$ | async } as vm">
      <div *ngIf="vm.loading">Ładowanie...</div>
      <ul *ngIf="!vm.loading">
        <li *ngFor="let user of vm.users">{{ user.name }}</li>
      </ul>
    </ng-container>
  `
})
export class UsersComponent {
  users$ = this.usersService.getUsers();
  currentUser$ = this.authService.currentUser$;
  loading$ = this.usersService.loading$;
}

takeUntilDestroyed (Angular 16+)

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({...})
export class ModernComponent {
  private destroyRef = inject(DestroyRef);

  ngOnInit() {
    this.someService.data$.pipe(
      takeUntilDestroyed(this.destroyRef)
    ).subscribe(data => {
      this.doSomething(data);
    });
  }
}

takeUntil pattern (przed Angular 16)

@Component({...})
export class ClassicComponent implements OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    this.someService.data$.pipe(
      takeUntil(this.destroy$)
    ).subscribe(data => {
      this.doSomething(data);
    });

    // Możesz mieć wiele subskrypcji z tym samym takeUntil
    this.anotherService.events$.pipe(
      takeUntil(this.destroy$)
    ).subscribe(event => {
      this.handleEvent(event);
    });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Kiedy który pattern?

Scenariusz Rozwiązanie
Wyświetlanie danych w szablonie async pipe
Side effects w komponencie takeUntilDestroyed lub takeUntil
Jednorazowy HTTP request Nie wymaga (kończy się sam)
Interval, WebSocket, ciągłe strumienie Zawsze unsubscribe!

Reactive Forms z RxJS

Angular Reactive Forms to showcase możliwości RxJS. Każdy FormControl eksponuje Observable:

@Component({
  template: `
    <form [formGroup]="form">
      <input formControlName="email">
      <div *ngIf="emailErrors$ | async as errors">
        <span *ngIf="errors.required">Email wymagany</span>
        <span *ngIf="errors.email">Nieprawidłowy format</span>
        <span *ngIf="errors.taken">Email zajęty</span>
      </div>
    </form>
  `
})
export class RegistrationComponent implements OnInit {
  form = new FormGroup({
    email: new FormControl('', [Validators.required, Validators.email])
  });

  emailErrors$: Observable<any>;

  ngOnInit() {
    // Reaktywna walidacja z async validator
    this.emailErrors$ = this.form.get('email')!.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap(email =>
        email ? this.checkEmailAvailable(email) : of(null)
      ),
      map(serverError => ({
        ...this.form.get('email')!.errors,
        ...serverError
      }))
    );
  }

  private checkEmailAvailable(email: string): Observable<{taken: true} | null> {
    return this.http.get<boolean>(`/api/check-email?email=${email}`).pipe(
      map(isTaken => isTaken ? { taken: true } : null),
      catchError(() => of(null))
    );
  }
}

Praktyczne wzorce dla rozmowy

Cache HTTP response

@Injectable({ providedIn: 'root' })
export class CachedApiService {
  private cache = new Map<string, Observable<any>>();

  getData(id: string): Observable<Data> {
    if (!this.cache.has(id)) {
      this.cache.set(
        id,
        this.http.get<Data>(`/api/data/${id}`).pipe(
          shareReplay(1) // Cache ostatniej wartości
        )
      );
    }
    return this.cache.get(id)!;
  }

  invalidateCache(id: string): void {
    this.cache.delete(id);
  }
}

Loading state

@Injectable({ providedIn: 'root' })
export class UsersService {
  private loadingSubject = new BehaviorSubject<boolean>(false);
  loading$ = this.loadingSubject.asObservable();

  getUsers(): Observable<User[]> {
    this.loadingSubject.next(true);

    return this.http.get<User[]>('/api/users').pipe(
      finalize(() => this.loadingSubject.next(false))
    );
  }
}

// W komponencie
@Component({
  template: `
    <div *ngIf="loading$ | async">Ładowanie...</div>
    <ul *ngIf="!(loading$ | async)">
      <li *ngFor="let user of users$ | async">{{ user.name }}</li>
    </ul>
  `
})
export class UsersComponent {
  users$ = this.usersService.getUsers();
  loading$ = this.usersService.loading$;
}

Polling API

@Injectable({ providedIn: 'root' })
export class StatusService {
  private pollingEnabled$ = new BehaviorSubject<boolean>(true);

  status$ = this.pollingEnabled$.pipe(
    switchMap(enabled =>
      enabled
        ? interval(5000).pipe(
            startWith(0),
            switchMap(() => this.http.get<Status>('/api/status')),
            retry(3),
            catchError(() => of({ status: 'unknown' }))
          )
        : EMPTY
    ),
    shareReplay(1)
  );

  stopPolling(): void {
    this.pollingEnabled$.next(false);
  }

  startPolling(): void {
    this.pollingEnabled$.next(true);
  }
}

Czego rekruterzy szukają

Po latach przeprowadzania rozmów na Angular Developer, wiem że rekruterzy zwracają uwagę na:

Rozumienie lazy evaluation - kandydaci często myślą, że Observable wykonuje się jak Promise. Musisz wiedzieć, że nic się nie dzieje bez subscribe (lub async pipe).

Wybór właściwego operatora - switchMap vs mergeMap to nie tylko teoria. Rekruterzy dają scenariusze: "Masz autocomplete, co użyjesz? A gdyby to był upload wielu plików?".

Zarządzanie subskrypcjami - jeśli nie wspomnisz o unsubscribe, takeUntil lub async pipe, to czerwona flaga. Wycieki pamięci w produkcji to poważny problem.

Praktyczne doświadczenie - opowiedz o konkretnych przypadkach: "W projekcie X użyłem shareReplay do cache'owania konfiguracji, co zmniejszyło liczbę requestów o 80%".

Praktyka przed rozmową

Zanim pójdziesz na rozmowę, przećwicz te zadania:

Zbuduj komponent wyszukiwania z debounce, który pokazuje wyniki z API i obsługuje błędy z komunikatem dla użytkownika.

Stwórz serwis zarządzający stanem koszyka zakupowego z BehaviorSubject, metodami add/remove/clear i Observable sumy.

Zaimplementuj polling endpoint ze statusem serwera, który zatrzymuje się gdy użytkownik przechodzi na inną zakładkę (visibility API + RxJS).

Napisz interceptor HTTP który dodaje token z serwisu AuthService i automatycznie odświeża token gdy wygaśnie (401 response).

Te zadania pokrywają 90% pytań praktycznych na rozmowach Angular.


Zobacz też


Chcesz więcej pytań z RxJS?

To tylko wycinek z 40 pytań rekrutacyjnych z RxJS, które przygotowaliśmy. Pełny zestaw fiszek pokrywa:

  • Wszystkie typy Subjects i kiedy ich używać
  • 30+ operatorów z przykładami użycia
  • Zaawansowane wzorce: multicasting, hot/cold Observable
  • Error handling i retry strategies
  • Integracja z Angular: HTTP, Forms, Router
  • Testing Observable z marble diagrams

Sprawdź pełny zestaw fiszek RxJS

Możesz też wypróbować bezpłatny podgląd pytań z RxJS, żeby zobaczyć jakość naszych materiałów.


Artykuł przygotowany przez zespół Flipcards na podstawie rzeczywistych doświadczeń z rozmów rekrutacyjnych na stanowiska Angular Developer.

Chcesz więcej pytań rekrutacyjnych?

To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.