Co nowego w Angular 19? Zoneless, Signals i @defer - kompletny przewodnik na rozmowę rekrutacyjną w 2025
Angular 19 wprowadził zmiany, które fundamentalnie zmieniają sposób pisania aplikacji - zoneless change detection eliminuje Zone.js z bundla, Signals zastępują tradycyjne podejście do reaktywności, a nowa składnia @defer rewolucjonizuje lazy loading. Na rozmowach rekrutacyjnych w 2026 roku te tematy będą absolutnym standardem, a kandydaci nieznający różnicy między signal() a computed() czy nieroumiejący wyjaśnić dlaczego Zone.js jest problematyczny, zostaną szybko odrzuceni.
W tym przewodniku znajdziesz wszystkie nowości Angular 19 z praktycznymi przykładami kodu i gotowymi odpowiedziami na pytania rekrutacyjne.
1. Zoneless change detection - Angular bez Zone.js
Wyjaśnienie w 30 sekund
Zoneless change detection to tryb w Angular 19, który pozwala uruchamiać aplikacje bez Zone.js. Zamiast automatycznego monkey-patchingu wszystkich asynchronicznych operacji, Angular używa Signals do precyzyjnego wykrywania zmian. Włącza się go przez
provideZonelessChangeDetection(). Korzyści to mniejszy bundle (~13KB mniej), lepsza wydajność, prostsze debugowanie i eliminacja "magicznego" zachowania Zone.js.
Wyjaśnienie w 2 minuty
Zone.js to biblioteka, która przez lata była sercem change detection w Angular. Działa poprzez monkey-patching natywnych API przeglądarki - setTimeout, Promise, addEventListener - żeby Angular wiedział, kiedy uruchomić wykrywanie zmian. Problem? Zone.js dodaje ~13KB do bundla, spowalnia asynchroniczne operacje i sprawia, że debugowanie jest koszmarem (stack trace'y są nieczytelne).
Angular 19 wprowadza alternatywę - aplikacje mogą działać całkowicie bez Zone.js:
// Konfiguracja aplikacji zoneless
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideZonelessChangeDetection(), // Włączamy tryb zoneless
provideRouter(routes),
provideHttpClient(),
]
}).catch(err => console.error(err));
Po włączeniu zoneless, Angular nie wykrywa automatycznie zmian po każdej asynchronicznej operacji. Zamiast tego, change detection uruchamia się gdy:
- Zmieni się wartość Signal używanego w szablonie
- Wywołamy
ChangeDetectorRef.markForCheck() - Użyjemy
PendingTasksdo śledzenia operacji asynchronicznych
import { Component, ChangeDetectionStrategy, signal, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { PendingTasks } from '@angular/core';
interface User {
id: number;
name: string;
}
@Component({
selector: 'app-user-list',
template: `
<h2>Użytkownicy</h2>
@for (user of users(); track user.id) {
<div class="user">{{ user.name }}</div>
}
<button (click)="loadUsers()">Odśwież</button>
`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserListComponent {
private http = inject(HttpClient);
private pendingTasks = inject(PendingTasks);
users = signal<User[]>([]);
loadUsers() {
// Dla SSR: śledzimy oczekujące operacje asynchroniczne
const taskCleanup = this.pendingTasks.add();
this.http.get<User[]>('/api/users').subscribe({
next: (data) => {
// Aktualizacja Signal automatycznie triggeruje change detection
this.users.set(data);
taskCleanup();
},
error: () => taskCleanup(),
});
}
}
Klasyczne pytanie rekrutacyjne
Pytanie: Dlaczego Zone.js jest problematyczny i jak zoneless rozwiązuje te problemy?
Odpowiedź: Zone.js ma trzy główne problemy. Po pierwsze, dodaje ~13KB do bundla i spowalnia każdą operację asynchroniczną przez dodatkowy overhead. Po drugie, monkey-patching wszystkich API sprawia, że stack trace'y są nieczytelne - zamiast prostego wywołania widzisz dziesiątki ramek Zone.js. Po trzecie, Zone.js uruchamia change detection po każdej asynchronicznej operacji, nawet jeśli nie zmieniła się żadna dana używana w widoku.
Zoneless rozwiązuje to wszystko - nie ma dodatkowego kodu w bundle, operacje asynchroniczne działają natywnie z czystymi stack trace'ami, a change detection uruchamia się tylko gdy faktycznie zmieni się Signal używany w szablonie.
2. Signals - reaktywność bez RxJS
Wyjaśnienie w 30 sekund
Signals to reaktywne prymitywy wprowadzone w Angular 16, które stały się fundamentem Angular 19. Signal to wrapper na wartość, który powiadamia Angular o zmianach. Mamy trzy typy:
signal()do wartości zapisywalnych,computed()do wartości obliczanych, ieffect()do side effects. Signals są synchroniczne, automatycznie integrują się z change detection i eliminują potrzebę subscribe/unsubscribe.
Wyjaśnienie w 2 minuty
Signals to odpowiedź Angulara na sukces reaktywności w innych frameworkach (Vue, Solid, Svelte). Zamiast RxJS do prostej reaktywności, mamy teraz lekkie prymitywy:
import { Component, computed, signal, effect, WritableSignal } from '@angular/core';
interface Product {
id: number;
name: string;
price: number;
quantity: number;
}
@Component({
selector: 'app-shopping-cart',
template: `
<h2>Koszyk</h2>
<div class="cart">
@for (item of cartItems(); track item.id) {
<div class="cart-item">
<span>{{ item.name }}</span>
<span>{{ item.price }} zł</span>
<input
type="number"
[value]="item.quantity"
(input)="updateQuantity(item.id, $event)">
<button (click)="removeItem(item.id)">Usuń</button>
</div>
}
</div>
<div class="summary">
<p>Liczba produktów: {{ totalItems() }}</p>
<p>Suma: {{ subtotal().toFixed(2) }} zł</p>
<p>VAT (23%): {{ tax().toFixed(2) }} zł</p>
<p><strong>Razem: {{ total().toFixed(2) }} zł</strong></p>
</div>
<button (click)="checkout()" [disabled]="isEmpty()">
Zamów
</button>
`,
standalone: true,
})
export class ShoppingCartComponent {
// Writable signal - można modyfikować
cartItems: WritableSignal<Product[]> = signal([
{ id: 1, name: 'Kurs Angular', price: 199.99, quantity: 1 },
{ id: 2, name: 'Kurs TypeScript', price: 149.99, quantity: 2 },
]);
// Computed signals - automatycznie przeliczane gdy zmienią się zależności
totalItems = computed(() =>
this.cartItems().reduce((sum, item) => sum + item.quantity, 0)
);
subtotal = computed(() =>
this.cartItems().reduce((sum, item) => sum + (item.price * item.quantity), 0)
);
tax = computed(() => this.subtotal() * 0.23);
total = computed(() => this.subtotal() + this.tax());
isEmpty = computed(() => this.cartItems().length === 0);
constructor() {
// Effect - wykonuje side effects gdy zmienią się zależności
effect(() => {
console.log(`Koszyk zaktualizowany: ${this.totalItems()} produktów`);
// Zapisz do localStorage
localStorage.setItem('cart', JSON.stringify(this.cartItems()));
});
// Wczytaj koszyk z localStorage
const savedCart = localStorage.getItem('cart');
if (savedCart) {
this.cartItems.set(JSON.parse(savedCart));
}
}
updateQuantity(itemId: number, event: Event) {
const input = event.target as HTMLInputElement;
const newQuantity = parseInt(input.value, 10);
// update() pozwala na modyfikację na podstawie poprzedniej wartości
this.cartItems.update(items =>
items.map(item =>
item.id === itemId
? { ...item, quantity: Math.max(1, newQuantity) }
: item
)
);
}
removeItem(itemId: number) {
this.cartItems.update(items => items.filter(item => item.id !== itemId));
}
checkout() {
if (!this.isEmpty()) {
console.log('Przetwarzanie zamówienia, suma:', this.total());
this.cartItems.set([]);
}
}
}
Diagram porównujący Signals vs RxJS:
Klasyczne pytanie rekrutacyjne
Pytanie: Kiedy używać Signals, a kiedy RxJS?
Odpowiedź: Signals są idealne do synchronicznego stanu komponentu - formularzy, UI state, prostych wartości obliczanych. Są prostsze (bez subscribe/unsubscribe), automatycznie integrują się z change detection i mają lepszą wydajność.
RxJS nadal jest lepszy do złożonych scenariuszy asynchronicznych - strumieni WebSocket, debounce/throttle na inputach, łączenia wielu źródeł danych (combineLatest, merge), retry logiki dla HTTP. Wzorzec, który mi się sprawdził: Signals dla "co wyświetlamy", RxJS dla "skąd to pobieramy".
3. Signal inputs i outputs - nowe API komponentów
Wyjaśnienie w 30 sekund
input()ioutput()to nowe funkcje zastępujące dekoratory@Input()i@Output(). Kluczowa różnica:input()zwraca Signal, więc jest reaktywny i automatycznie triggeruje change detection. Wspierainput.required<T>()dla wymaganych inputów, transformacje wartości i lepsze typowanie.output()używaOutputEmitterRefzamiastEventEmitter.
Wyjaśnienie w 2 minuty
Nowe API input() i output() rozwiązuje kilka problemów starych dekoratorów. Po pierwsze, inputy są teraz Signals, więc możesz używać ich w computed() i effect(). Po drugie, masz lepsze typowanie - input.required<T>() wymusza przekazanie wartości. Po trzecie, możesz definiować transformacje inline. Oto kompleksowy przykład:
import { Component, input, output, computed } from '@angular/core';
interface User {
id: number;
name: string;
}
@Component({
selector: 'app-user-profile',
template: `
<div class="profile">
<h2>{{ displayName() }}</h2>
<p>ID: {{ userId() }}</p>
@if (isAdmin()) {
<span class="badge">Administrator</span>
}
<button (click)="handleEdit()">Edytuj profil</button>
<button (click)="handleDelete()">Usuń</button>
</div>
`,
standalone: true,
})
export class UserProfileComponent {
// Signal input - wymagany, reaktywny
user = input.required<User>();
// Opcjonalny input z domyślną wartością
showId = input(true);
// Input z transformacją wartości
role = input('user', {
transform: (value: string) => value.toLowerCase()
});
// Signal output - emituje zdarzenia
userEdit = output<User>();
profileDeleted = output<void>();
// Computed signals - reagują na zmiany inputów
displayName = computed(() => {
const currentUser = this.user();
return `${currentUser.name} (#${currentUser.id})`;
});
userId = computed(() => this.showId() ? this.user().id : null);
isAdmin = computed(() => this.role() === 'admin');
handleEdit() {
// Emituj zdarzenie z danymi
this.userEdit.emit(this.user());
}
handleDelete() {
// Emituj zdarzenie bez danych
this.profileDeleted.emit();
}
}
// Użycie w komponencie rodzica:
// <app-user-profile
// [user]="currentUser"
// [showId]="true"
// role="Admin"
// (userEdit)="onEditUser($event)"
// (profileDeleted)="onDelete()" />
Porównanie starego i nowego API:
// ❌ Stary sposób - dekoratory
@Component({ ... })
export class OldComponent {
@Input() user!: User;
@Input() showId = true;
@Output() userEdit = new EventEmitter<User>();
// Brak reaktywności - trzeba używać ngOnChanges
ngOnChanges(changes: SimpleChanges) {
if (changes['user']) {
this.updateDisplayName();
}
}
}
// ✅ Nowy sposób - signal inputs
@Component({ ... })
export class NewComponent {
user = input.required<User>();
showId = input(true);
userEdit = output<User>();
// Reaktywność wbudowana - computed automatycznie reaguje
displayName = computed(() => `${this.user().name}`);
}
4. Nowy control flow - @if, @for, @switch
Wyjaśnienie w 30 sekund
Angular 17+ wprowadził nową składnię control flow:
@if,@for,@switchzamiast*ngIf,*ngFor,*ngSwitch. Nowa składnia jest szybsza (optymalizacje kompilatora), prostsza (bez ng-template), wspiera@else ifbezpośrednio, i dodaje@letdo deklaracji zmiennych lokalnych. Jest type-safe i ma lepsze komunikaty błędów.
Wyjaśnienie w 2 minuty
Nowa składnia control flow to nie tylko kosmetyczna zmiana - kompilator może ją lepiej optymalizować, bo zna strukturę w compile-time. Dodatkowo @else if działa bez dodatkowych ng-template, a @for wymaga track co eliminuje problemy wydajnościowe. Porównaj stary i nowy sposób:
@Component({
selector: 'app-product-list',
template: `
<!-- @if z @else if i @else -->
@if (loading()) {
<div class="spinner">Ładowanie...</div>
} @else if (error()) {
<div class="error">Błąd: {{ error() }}</div>
} @else if (products().length === 0) {
<div class="empty">Brak produktów</div>
} @else {
<!-- @for z track i @empty -->
@for (product of products(); track product.id) {
<div class="product">
<h3>{{ product.name }}</h3>
<p>{{ product.price }} zł</p>
<!-- @switch dla statusu -->
@switch (product.status) {
@case ('available') {
<span class="badge green">Dostępny</span>
}
@case ('low-stock') {
<span class="badge yellow">Ostatnie sztuki</span>
}
@case ('out-of-stock') {
<span class="badge red">Niedostępny</span>
}
@default {
<span class="badge gray">Nieznany status</span>
}
}
</div>
} @empty {
<p>Lista jest pusta</p>
}
}
<!-- @let do deklaracji zmiennych lokalnych -->
@let total = calculateTotal();
@let discountedTotal = total * 0.9;
<div class="summary">
<p>Suma: {{ total }} zł</p>
<p>Po rabacie: {{ discountedTotal }} zł</p>
</div>
`,
standalone: true,
})
export class ProductListComponent {
loading = signal(false);
error = signal<string | null>(null);
products = signal<Product[]>([]);
calculateTotal() {
return this.products().reduce((sum, p) => sum + p.price, 0);
}
}
Porównanie starej i nowej składni:
<!-- ❌ Stary sposób - *ngIf z ng-template -->
<div *ngIf="loading; else notLoading">Ładowanie...</div>
<ng-template #notLoading>
<div *ngIf="error; else showContent">{{ error }}</div>
</ng-template>
<ng-template #showContent>
<div *ngFor="let item of items; trackBy: trackById">
{{ item.name }}
</div>
</ng-template>
<!-- ✅ Nowy sposób - @if/@for -->
@if (loading) {
<div>Ładowanie...</div>
} @else if (error) {
<div>{{ error }}</div>
} @else {
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}
}
Klasyczne pytanie rekrutacyjne
Pytanie: Dlaczego track w @for jest wymagany, a trackBy w *ngFor był opcjonalny?
Odpowiedź: Angular wymusił track w nowej składni, ponieważ brak trackowania jest częstym źródłem problemów wydajnościowych. Bez track, Angular musi porównywać obiekty przez referencję i przebudowywać całą listę przy każdej zmianie. Z track, Angular wie które elementy się zmieniły i aktualizuje tylko je. Wymuszenie track od początku eliminuje całą klasę bugów wydajnościowych.
5. @defer - lazy loading na poziomie szablonu
Wyjaśnienie w 30 sekund
@deferto nowa składnia do lazy loadingu komponentów bezpośrednio w szablonie. Pozwala opóźnić ładowanie ciężkich komponentów do momentu spełnienia warunku - widoczność w viewport, interakcja użytkownika, timer lub custom condition. Składnia zawiera bloki@placeholder,@loadingi@errordla różnych stanów.
Wyjaśnienie w 2 minuty
@defer to game-changer dla wydajności aplikacji Angular. Zamiast ładować wszystko na starcie, możesz opóźnić ładowanie ciężkich komponentów do momentu gdy są naprawdę potrzebne. Angular automatycznie tworzy osobny chunk i ładuje go w odpowiednim momencie. Oto praktyczne przykłady różnych triggerów:
@Component({
selector: 'app-dashboard',
template: `
<h1>Dashboard</h1>
<!-- Podstawowy @defer - ładuje gdy przeglądarka jest idle -->
@defer {
<app-heavy-chart [data]="chartData()" />
} @placeholder {
<div class="skeleton">Przygotowywanie wykresu...</div>
} @loading (minimum 500ms) {
<div class="spinner">Ładowanie wykresu...</div>
} @error {
<div class="error">Nie udało się załadować wykresu</div>
}
<!-- @defer on viewport - ładuje gdy element jest widoczny -->
@defer (on viewport) {
<app-comments [postId]="postId()" />
} @placeholder {
<div class="comments-placeholder">
Przewiń aby zobaczyć komentarze
</div>
}
<!-- @defer on interaction - ładuje po kliknięciu -->
<button #loadBtn>Pokaż statystyki</button>
@defer (on interaction(loadBtn)) {
<app-statistics />
} @placeholder {
<p>Kliknij przycisk aby załadować statystyki</p>
}
<!-- @defer on hover - ładuje gdy użytkownik najedzie myszką -->
<div #hoverArea class="hover-trigger">
Najedź myszką aby załadować podgląd
</div>
@defer (on hover(hoverArea)) {
<app-preview-card />
}
<!-- @defer on timer - ładuje po określonym czasie -->
@defer (on timer(3s)) {
<app-recommendations />
} @placeholder {
<p>Rekomendacje załadują się za chwilę...</p>
}
<!-- @defer when - ładuje gdy warunek jest spełniony -->
@defer (when isLoggedIn()) {
<app-user-settings />
} @placeholder {
<p>Zaloguj się aby zobaczyć ustawienia</p>
}
<!-- Kombinacja triggerów -->
@defer (on viewport; on timer(5s); prefetch on idle) {
<app-newsletter-signup />
}
`,
standalone: true,
})
export class DashboardComponent {
chartData = signal([]);
postId = signal(1);
isLoggedIn = signal(false);
}
Diagram działania @defer:
Dostępne triggery @defer:
| Trigger | Opis | Przykład |
|---|---|---|
on idle |
Gdy przeglądarka jest idle (domyślny) | @defer (on idle) |
on viewport |
Gdy element jest widoczny | @defer (on viewport) |
on interaction |
Po kliknięciu/focus | @defer (on interaction(btn)) |
on hover |
Po najechaniu myszką | @defer (on hover(element)) |
on timer |
Po określonym czasie | @defer (on timer(2s)) |
when |
Gdy warunek jest true | @defer (when isReady()) |
prefetch |
Preload przed triggerem | @defer (prefetch on idle) |
6. Migracja z Angular 18 do Angular 19
Checklist migracji
- Zaktualizuj Angular CLI i pakiety:
ng update @angular/core@19 @angular/cli@19
- Włącz zoneless (opcjonalnie):
// main.ts
bootstrapApplication(AppComponent, {
providers: [
provideZonelessChangeDetection(),
// ... inne providery
]
});
- Migruj @Input/@Output na signal inputs:
ng generate @angular/core:signal-input-migration
- **Zamień ngIf/ngFor na @if/@for:
ng generate @angular/core:control-flow-migration
- Usuń Zone.js z bundla (jeśli zoneless):
// angular.json - usuń zone.js z polyfills
"polyfills": [
// "zone.js" <- usuń tę linię
]
7. Computed vs Effect - kiedy którego użyć
Wyjaśnienie w 30 sekund
computed()służy do tworzenia wartości obliczanych z innych Signals - jest czysty, bez side effects, i automatycznie cachuje wynik.effect()służy do wykonywania side effects (logowanie, localStorage, API calls) gdy zmienią się Signals. Złota zasada: jeśli potrzebujesz wartości, użyj computed; jeśli potrzebujesz akcji, użyj effect.
Wyjaśnienie w 2 minuty
Różnica jest fundamentalna: computed() to czysta funkcja która zwraca wartość, effect() to procedura która wykonuje akcje. Computed automatycznie śledzi zależności i cachuje wynik - przelicza się tylko gdy zmienią się źródłowe Signals. Effect wykonuje się za każdym razem gdy zmieni się którykolwiek z używanych Signals. Typowe zastosowania:
@Component({
selector: 'app-example',
template: `
<input [value]="searchTerm()" (input)="updateSearch($event)">
<p>Wyniki dla: {{ normalizedSearch() }}</p>
<p>Znaleziono: {{ filteredResults().length }} wyników</p>
`,
})
export class ExampleComponent {
searchTerm = signal('');
allItems = signal<Item[]>([]);
// ✅ computed - czysta funkcja, zwraca wartość
normalizedSearch = computed(() =>
this.searchTerm().trim().toLowerCase()
);
// ✅ computed - filtrowanie na podstawie innych signals
filteredResults = computed(() => {
const search = this.normalizedSearch();
if (!search) return this.allItems();
return this.allItems().filter(item =>
item.name.toLowerCase().includes(search)
);
});
constructor() {
// ✅ effect - side effect (logowanie, analytics)
effect(() => {
const term = this.searchTerm();
if (term.length > 2) {
console.log('Użytkownik szuka:', term);
this.analytics.trackSearch(term);
}
});
// ✅ effect - synchronizacja z zewnętrznym systemem
effect(() => {
localStorage.setItem('lastSearch', this.searchTerm());
});
}
updateSearch(event: Event) {
const input = event.target as HTMLInputElement;
this.searchTerm.set(input.value);
}
}
Błędy, których należy unikać:
// ❌ ŹLE - modyfikowanie Signal w computed
badComputed = computed(() => {
this.someSignal.set(123); // BŁĄD! computed musi być czysty
return this.otherSignal() * 2;
});
// ❌ ŹLE - tworzenie effect wewnątrz computed
anotherBadComputed = computed(() => {
effect(() => console.log('test')); // BŁĄD! NG0602
return this.value();
});
// ❌ ŹLE - toSignal wewnątrz computed
yetAnotherBad = computed(() => {
const data = toSignal(this.http.get('/api')); // BŁĄD!
return data();
});
// ✅ DOBRZE - toSignal poza computed
dataSignal = toSignal(this.http.get('/api'));
processedData = computed(() => this.dataSignal()?.map(/* ... */));
Na co rekruterzy naprawdę zwracają uwagę
Po przeprowadzeniu wielu rozmów rekrutacyjnych z pytaniami o Angular 19, mogę powiedzieć że rekruterzy sprawdzają:
Czy rozumiesz "dlaczego" zoneless - nie wystarczy wiedzieć jak włączyć. Kandydaci, którzy robią wrażenie, potrafią wyjaśnić problemy Zone.js (bundle size, performance overhead, nieczytelne stack traces) i jak Signals je rozwiązują.
Czy znasz różnicę między signal() a computed() a effect() - to fundamentalne pytanie. Signal to wartość zapisywalna, computed to wartość obliczana (czysta, cachowana), effect to side effect. Jeśli ktoś myli te koncepty, od razu widać brak praktycznego doświadczenia.
Czy potrafisz migrować istniejący kod - pytania typu "masz komponent z @Input i ngOnChanges, jak go zmigrujesz na signal inputs?" są bardzo popularne. Sprawdzają czy kandydat naprawdę pracował z nowym API.
Czy wiesz kiedy @defer ma sens - lazy loading wszystkiego to antywzorzec. Dobry kandydat wie, że @defer jest dla ciężkich komponentów below the fold, a nie dla każdego elementu.
Zobacz też
- Kompletny Przewodnik - Rozmowa Frontend Developer - pełny przewodnik przygotowania do rozmowy frontend
- Angular vs React - porównanie dla programistów 2026 - które technologie wybrać i jak się różnią
- TOP 5 błędów w zadaniach rekrutacyjnych z Angulara - najczęstsze pułapki na rozmowach
- Najtrudniejsze pytania z frameworków JavaScript - React, Angular, Vue i inne
Chcesz więcej pytań z Angulara?
Ten artykuł to tylko wierzchołek góry lodowej. Mamy ponad 150 pytań z Angular z szczegółowymi odpowiedziami, przykładami kodu i wyjaśnieniami - od podstaw po zaawansowane tematy jak Signals, Dependency Injection i optymalizacja wydajności.
Sprawdź Pełny Zestaw Pytań Angular →
Lub wypróbuj nasz darmowy podgląd pytań Angular, żeby zobaczyć więcej pytań w tym stylu.
Napisane przez zespół Flipcards, na podstawie oficjalnej dokumentacji Angular 19 i doświadczeń z rozmów rekrutacyjnych w firmach takich jak Google, Microsoft i wiodących software house'ach.
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.
