Angular
How an Angular app is put together, how the framework learns about changes, how to work with RxJS and what changed from Angular 13 to the current version 22. Interviewers use this to check both the fundamentals and whether you can maintain or upgrade an older app.
Before this topic: JavaScript and TypeScript.
What is an Angular app made of?
Section titled “What is an Angular app made of?”What it is
An Angular app is a tree of components. The root component is bootstrapped in main.ts, and the rest are nested in its template.
- Component — a class with the
@Componentdecorator plus an HTML template. The class holds data and handlers, the template displays them. - Service — a class with shared logic: API calls, a cache, the current user.
- Router — shows the component that matches the URL.
How to use
ng generate component orderscreates a component.- List the child components and directives the template needs in the decorator’s
imports. - Get a service as a class field:
private orders = inject(OrderService).
Common mistake
Forgetting to add a child component to imports. The build fails with 'app-x' is not a known element.
Takeaway: the component displays data, the service fetches it, and DI wires one to the other.
How it works
| Template syntax | What it does |
|---|---|
{{ total }} |
interpolation: a class value into text |
[disabled]="busy" |
property binding: class → DOM |
(click)="save()" |
event binding: DOM → class |
[(value)]="name" |
two-way: a property plus a valueChange event |
- A directive changes how an element behaves:
[class.active],ngModel. A component is a directive with its own template. - The structural directives
*ngIfand*ngForwere replaced by the built-in@ifand@forblocks in Angular 17. - NgModule vs standalone. In Angular 13 a component had to be declared in some NgModule’s
declarations. Since v19 components are standalone by default and list their dependencies in their ownimports.
When to choose what
| How to get a service | When |
|---|---|
inject(Service) in a field |
by default; the Angular style guide recommends it |
| constructor parameter | legacy code; works as before |
Pitfalls
inject()only works in an injection context: field initializers, the constructor, provider factories, guards. Calling it in an event handler throws NG0203.- A service in a component’s
providersis a separate instance for every component instance, not a shared one.
Rule of thumb
New components are standalone, services use providedIn: 'root', dependencies come through inject().
Takeaway: bindings tie the template to the class, and the component lists its dependencies in imports and gets them through inject().
Under the hood
- The AOT compiler turns a template into functions that create and update the DOM. Type errors in bindings show up at build time.
- Provider lookup goes bottom-up: first the element injectors from the component up to the root of the tree, then the environment injectors — route, root, platform. Nothing found —
NullInjectorError.
Compared with ASP.NET Core
| Angular | Closest ASP.NET Core equivalent |
|---|---|
providedIn: 'root', @Service() (v22) |
AddSingleton |
route providers |
an instance per app section; no direct equivalent |
component providers |
an instance per component and its subtree; .NET scopes are not hierarchical |
inject(X, { optional: true }) |
GetService instead of GetRequiredService |
Trade-offs
- Standalone makes a component’s dependencies explicit and lets you lazy-load a single component. NgModule is still supported, and the
ng generate @angular/core:standalonemigration converts it. @Service()from v22 is shorthand for@Injectable({ providedIn: 'root' }). It only works withinject(), not with constructor parameters.inject()makes inheritance simpler: a base class doesn’t needsuper(...)with a long list of dependencies.- The hierarchy lets a page own its state through
providers, but the same mechanism quietly duplicates singletons.
How to check
Angular DevTools in a dev build shows the component and injector trees and where each service came from. Duplicate singletons are visible there too.
Takeaway: DI in Angular is hierarchical: where you declare a provider is where the shared instance ends.
import { Component, Injectable, inject } from '@angular/core';
@Injectable({ providedIn: 'root' }) // one instance per app, like AddSingletonexport class GreetingService { greet(name: string) { return `Hello, ${name}!`; }}
@Component({ selector: 'app-greeting', // standalone by default since v19 templateUrl: './greeting.html',})export class Greeting { private readonly greetings = inject(GreetingService); // instead of a constructor parameter protected message = '';
protected say(name: string) { this.message = this.greetings.greet(name); }}<input #name value="Angular" /><button (click)="say(name.value)">Greet</button><p [class.empty]="!message">{{ message }}</p> <!-- after a click: Hello, Angular! -->- How does
providedIn: 'root'differ fromprovidersin a component? — The first gives one instance per app, the second a separate instance for each component instance. - Where can you call
inject()? — In an injection context: field initializers, the constructor, factories, guards and interceptors; otherwise NG0203. - Do you need an NgModule in a new app? — No: since v19 components are standalone by default; NgModule stays for legacy code.
- How is a component different from a directive? — A component is a directive with its own template; a directive changes the behavior of an existing element.
- What is a tree-shakable provider? — With
providedIn: 'root', a service that is never injected doesn’t end up in the bundle.
How does change detection work: zone.js, OnPush, signals and zoneless?
Section titled “How does change detection work: zone.js, OnPush, signals and zoneless?”What it is
Change detection is the check of whether what’s on screen matches component data. If a value in a binding changed, Angular updates that piece of the DOM.
What changed since Angular 13
- Back then zone.js triggered the check after every click, timer or HTTP response — across the whole tree.
- Today state lives in signals:
signal(0)creates a value,count()reads it,count.set(1)changes it. Angular knows which templates read a signal and updates only those. - Since v21 new apps run without zone.js, and since v22 components are OnPush by default.
Common mistake
Mutating an array inside a signal: items().push(x). The reference is the same, so the signal reports nothing. Use items.update(list => [...list, x]).
Takeaway: Angular updates the screen when it learns about a change, and signals tell it precisely.
How it works with zone.js
- zone.js patches the browser’s async APIs:
setTimeout,Promise,addEventListener, XHR. - When a task and its microtasks finish,
NgZonereports it, and Angular callsApplicationRef.tick(). - The component tree is checked from the top down: every template expression is compared with its previous value.
- In dev mode a second pass catches values that changed during the check —
ExpressionChangedAfterItHasBeenCheckedError.
Strategies
| Strategy | When the component is checked |
|---|---|
Eager (formerly Default) |
on every cycle |
OnPush (default since v22) |
a new input by reference, an event in the subtree, markForCheck(), a change in a signal it reads |
Signals
signal holds a value, computed derives one — lazily and with caching, effect runs a side effect: logging, writing to localStorage, calling a third-party library.
Pitfalls
- Mutating an object passed as an input to an OnPush component: the reference is the same, so no check happens.
- A field changed in
setTimeoutor a subscription won’t render in an OnPush component without a signal ormarkForCheck(). - An
effectthat copies one signal into another. Derived values belong incomputedandlinkedSignal.
Rule of thumb
State in signals, immutable inputs, derived values through computed.
Takeaway: zone.js says “something happened”, signals say “this is what changed”.
Under the hood
- Signals form a graph: templates,
computedandeffectare consumers, signals are producers. Reading a signal in a reactive context registers a dependency. set()marks the dependent views and schedules a check. OnPush ancestors serve only as a path to them and are not re-checked themselves.- Without zone.js a check is triggered only by notifications:
markForCheck()(which theasyncpipe calls),ComponentRef.setInput, a change in a signal read by a template, an event handler bound in a template.
The cost of zone.js
- A check after every async event, even when no data changed.
- Bundle weight, startup time and long stack traces.
- zone.js can’t intercept native
async/await, so the build rewrites it into generators.
Trade-offs when migrating
ng updateto v21 addsprovideZoneChangeDetection(), and to v22 addsChangeDetectionStrategy.Eagerto components without an explicit strategy. Behavior is preserved.- Components are made OnPush-compatible: signals, immutable inputs, the
asyncpipe. NgZone.onStableandonMicrotaskEmptynever fire in zoneless mode — replace them withafterNextRender.- Last, remove
provideZoneChangeDetection()and dropzone.jsfrompolyfills.
How to check
- In a dev build,
provideCheckNoChangesConfig({ exhaustive: true, interval: 1000 })finds bindings that changed without a notification. - The Angular DevTools profiler shows how long each cycle takes and which components it checked.
Takeaway: zoneless is a shift from “check everything after any event” to “update what you were told about”, and readiness for it depends on component code, not on a flag.
import { ChangeDetectionStrategy, Component, computed, effect, signal } from '@angular/core';
@Component({ selector: 'app-cart', templateUrl: './cart.html', // OnPush is the default since v22; in Angular 13 you set it explicitly changeDetection: ChangeDetectionStrategy.OnPush,})export class Cart { protected readonly items = signal<{ name: string; price: number }[]>([]); // Recomputed only when items changes protected readonly total = computed(() => this.items().reduce((sum, i) => sum + i.price, 0));
constructor() { effect(() => console.log('Items:', this.items().length)); // a side effect, not state }
protected add() { // A new reference: the signal reports the change. // this.items().push(...) won't work — same reference this.items.update(list => [...list, { name: 'Book', price: 10 }]); }}<button (click)="add()">Add</button><p>Items: {{ items().length }}, total: {{ total() }}</p> <!-- after a click: Items: 1, total: 10 -->import { bootstrapApplication } from '@angular/platform-browser';import { provideZoneChangeDetection, provideZonelessChangeDetection } from '@angular/core';import { App } from './app/app';
// Angular 20: enable zoneless explicitly; since v21 it is the defaultbootstrapApplication(App, { providers: [provideZonelessChangeDetection()] });
// An older app updated to v21+ with ng update stays on zone.js like this:// bootstrapApplication(App, { providers: [provideZoneChangeDetection()] });- Why didn’t an OnPush component update after
this.user.name = 'X'? — The object reference didn’t change; you need a new object or a signal. - How does
computeddiffer fromeffect? —computedreturns a value and is cached;effectonly runs a side effect. - What does
ExpressionChangedAfterItHasBeenCheckedErrormean? — The dev-mode check found a binding that changed during the change detection cycle. - What does an app need to run without zone.js? — Notify Angular through signals,
markForCheckor theasyncpipe, and don’t rely onNgZone.onStable. - Do you need
NgZone.runOutsideAngularin zoneless mode? — No, but the call is harmless; in a zone-based app it avoids extra checks on frequent events.
How do you work with RxJS in Angular?
Section titled “How do you work with RxJS in Angular?”What it is
An Observable is a source that, once subscribed to, emits values — zero, one or many — and then completes or fails with an error.
| Property | Promise | Observable |
|---|---|---|
| When work starts | immediately on creation | on subscription |
| How many values | one | any number |
| Cancellation | none | unsubscribing (unsubscribe()) |
How to use
http.get<User[]>('/api/users')returns an Observable; the request goes out on subscription.- In a template:
users$ | async— subscribes and unsubscribes automatically. - In code:
.subscribe(users => ...).
Common mistake
Subscribing to a stream that never completes on its own (interval, router.events, a Subject in a service) and not unsubscribing. The component is destroyed, but the subscription lives on and keeps it in memory.
Takeaway: an Observable is lazy, multi-valued and cancellable, and every long-lived subscription has to be closed.
How it works
HttpClientemits one value and completes. Every subscription is a new request; unsubscribing before the response cancels it.- Operators in
pipe()build a new stream:map,filter,debounceTime,catchError.
When to choose what
| Operator | New value while the previous one is running | Example |
|---|---|---|
switchMap |
cancels the previous one | search as you type |
mergeMap |
runs in parallel | uploading independent files |
concatMap |
queues | saves where order matters |
exhaustMap |
ignores the new one | a Submit button, login |
How to unsubscribe
- The
asyncpipe — in templates. toSignal(obs$)turns a stream into a signal and unsubscribes when the component is destroyed;toObservable(sig)goes the other way.takeUntilDestroyed()(stable since v19) — for a manualsubscribe. Outside an injection context pass it aDestroyRef.
Pitfalls
- Two
asyncpipes on one HTTP stream mean two requests. Share the result withshareReplayortoSignal. takeUntil(destroy$)not last inpipe(): operators after it can keep the subscription alive.- A nested
subscribeinsidesubscribeinstead ofswitchMap: subscriptions aren’t cancelled and errors get lost.
Rule of thumb
Let the template or toSignal create the subscription; a manual subscribe only with takeUntilDestroyed().
Takeaway: the higher-order operator defines what happens in a race, and the way you subscribe defines how long the subscription lives.
Under the hood
- An Observable is a subscribe function.
pipe()starts nothing;subscribe()builds the chain of subscribers back to the source. - Unsubscribing travels up the chain. For
HttpClientthat cancels the request:AbortControllerin the fetch backend (the default since v22) orabort()for XHR. toObservableemits through aneffect, so severalset()calls in a row produce one value — the last one.
Cost
switchMapcancels the response on the client but not the work on the server. For POST and PUT that means lost or duplicated changes, so writes go throughconcatMaporexhaustMap.mergeMapwithout a concurrency limit (mergeMap(fn, 4)) can fire hundreds of requests at once.shareReplay(1)withoutrefCount: truekeeps its source subscription even after every subscriber has left.
Trade-offs
| Task | Tool |
|---|---|
| state for a template | a signal |
| loading data by parameters | httpResource, rxResource (stable since v22) |
| events over time, WebSocket, debounce, retry | RxJS |
How to check
- A leak shows up in Chrome DevTools: a heap snapshot after navigating back and forth a few times reveals detached elements and live component instances.
- In the Network tab cancelled requests are marked
(canceled), and duplicate requests give away an extra subscription.
Takeaway: RxJS is a tool for event streams and concurrency, not for holding state; state belongs in signals.
import { Component, inject } from '@angular/core';import { HttpClient } from '@angular/common/http';import { FormControl, ReactiveFormsModule } from '@angular/forms';import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';import { debounceTime, distinctUntilChanged, interval, switchMap } from 'rxjs';
@Component({ selector: 'app-product-search', imports: [ReactiveFormsModule], templateUrl: './product-search.html' })export class ProductSearch { private readonly http = inject(HttpClient); protected readonly query = new FormControl('', { nonNullable: true });
// switchMap cancels the previous request if the user keeps typing protected readonly products = toSignal( this.query.valueChanges.pipe( debounceTime(300), distinctUntilChanged(), switchMap(q => this.http.get<{ id: number; name: string }[]>('/api/products', { params: { q } })), ), { initialValue: [] }, // unsubscribes automatically when the component is destroyed );
constructor() { // An endless stream: without takeUntilDestroyed the subscription outlives the component interval(60_000).pipe(takeUntilDestroyed()).subscribe(n => console.log('tick', n)); }}<input [formControl]="query" placeholder="Search" />@for (p of products(); track p.id) { <p>{{ p.name }}</p>}- How is an Observable different from a Promise? — Lazy, multi-valued and cancellable; a Promise runs immediately and yields one value.
- Which operator for a Save button? —
exhaustMapto ignore repeated clicks, orconcatMapif every click matters, in order. - Do you need to unsubscribe from
HttpClient? — It completes on its own, but unsubscribing cancels the request and keeps the callback from running on a destroyed component. - How does
toSignaldiffer from theasyncpipe? — It gives a signal you can read in code and incomputed; both unsubscribe automatically. - Why does
takeUntilgo last? — Operators after it, such asswitchMap, can keep an inner subscription alive after the stop signal.
What has changed in Angular from version 13 to the current one?
Section titled “What has changed in Angular from version 13 to the current one?”What it is
Angular 13 shipped in November 2021; the current version is 22 (June 2026). In that time the coding style itself changed.
The main changes
- Components without NgModule — standalone.
- New template syntax:
@if,@for,@switchinstead of*ngIfand*ngFor. @defer— lazy loading part of a template.- Signals: state,
input()inputs andoutput()outputs. inject()instead of constructor parameters.- Builds on esbuild and Vite — noticeably faster than Webpack.
- SSR with hydration — the
@angular/ssrpackage instead of Angular Universal. - Running without zone.js and OnPush by default.
Common mistake
Thinking old code stopped working. NgModule, @Input() and *ngIf (deprecated since v20) are still supported, and ng update fixes what breaks.
Takeaway: modern Angular means standalone components, signals and the new template syntax; the old style works but is on its way out.
What arrived when
| Version | What changed |
|---|---|
| 14–15 (2022) | standalone components, typed forms, inject(), functional guards and interceptors |
| 16 (May 2023) | signals (preview), takeUntilDestroyed, hydration (preview), esbuild builds (preview) |
| 17 (November 2023) | @if, @for, @defer; stable signals; application builder and Vite for new projects |
| 17.1–19 | input(), output(), model(), signal queries; stable since v19, standalone by default |
| 20 (May 2025) | stable effect, linkedSignal, toSignal; *ngIf and *ngFor deprecated; zoneless stable since 20.2 |
| 21 (November 2025) | zoneless and Vitest by default in new projects; experimental Signal Forms |
| 22 (June 2026) | OnPush by default; stable Signal Forms, resource and httpResource; @Service(); HttpClient on fetch |
Migration pitfalls
ng updatemoves only one major version at a time. From 13 to 22 that’s nine steps, each with its own Node.js and TypeScript requirements.- Third-party libraries are upgraded together with Angular: Kendo UI for Angular, angular-oauth2-oidc and others must support the target version.
- Custom Webpack configuration doesn’t carry over to the application builder.
Rule of thumb
First bring the versions up to date while preserving behavior, then modernize the code with ng generate @angular/core:... migrations — one at a time.
Takeaway: since v13 Angular has moved from NgModule and zone.js to standalone and signals, and upgrades go one major at a time.
Why it changed
- Signals and zoneless remove the main cost of old Angular — checking the whole tree on every event.
- Standalone and
@defermake the component, not the module, the unit of loading. - esbuild and Vite speed up builds; the Webpack builders were deprecated in v22.
- Since v22 a major release ships every 12 months instead of every six. v23 is expected around June 2027.
Migration strategy
- Upgrade major by major following the update guide, building and running tests after each step.
- Automatic migrations preserve behavior: v21 adds
provideZoneChangeDetection()to the app, v22 addsChangeDetectionStrategy.Eagerto components without a strategy. - Switch the builder with
ng update @angular/cli --name use-application-builder. - Then one at a time: standalone, control flow,
inject(),input()andoutput(), lazy routes. - Last come OnPush and zoneless: they change logic, not syntax.
Cost
- Each major raises the minimum TypeScript and Node.js: v22 needs TypeScript 6.0 and Node.js 22.22+ or 24.13+.
- Old APIs get removed:
ComponentFactoryResolverin v22.HttpClientmoved to fetch, and upload progress needswithXhr(). - As of October 2026, Kendo UI for Angular 25.x supports Angular 20–22, and angular-oauth2-oidc 22.x requires Angular 22.
Trade-offs
There’s no need to rewrite everything at once: the old and new styles coexist in one app. New code is written in the new style, old code is converted when it’s touched.
Takeaway: migrating from 13 means a safe version upgrade that preserves behavior first, then a gradual move to signals and zoneless.
import { Component, EventEmitter, Input, Output, inject, input, output } from '@angular/core';import { HttpClient } from '@angular/common/http';
interface User { id: number; name: string; }
// Angular 13: decorators, constructor, declared in some NgModule's declarations@Component({ selector: 'app-user-old', templateUrl: './user-old.component.html' })export class UserOldComponent { @Input() user!: User; @Output() selected = new EventEmitter<User>(); constructor(private http: HttpClient) {}}
// Angular 22: standalone, signal inputs and outputs, inject()@Component({ selector: 'app-user', templateUrl: './user.html' })export class UserCard { readonly user = input.required<User>(); // read as user() readonly selected = output<User>(); private readonly http = inject(HttpClient);}<!-- Angular 13 --><li *ngFor="let o of orders; trackBy: trackById">{{ o.title }}</li><p *ngIf="!orders.length">No orders</p>
<!-- Angular 22: track is required, @empty instead of a separate *ngIf -->@for (o of orders(); track o.id) { <li>{{ o.title }}</li>} @empty { <p>No orders</p>}
<!-- The chart code loads when the block scrolls into view -->@defer (on viewport) { <app-sales-chart />} @placeholder { <p>Chart</p>}- Can you mix NgModule and standalone? — Yes: a standalone component can be imported into an NgModule, and an NgModule into a component’s
imports. - What does
trackdo in@for? — It sets the key Angular uses to match items to DOM nodes; in@forit is required. - How is
@deferdifferent from a lazy route? — It defers loading part of a template on a trigger (viewport, idle, interaction), not a whole page. - Why can’t you jump from 13 straight to 22? —
ng updatesupports only one major version at a time, and migrations are designed per step. - What is hydration? — The client reuses the DOM rendered on the server instead of building it again.
How do you structure a large Angular app and manage state?
Section titled “How do you structure a large Angular app and manage state?”What it is
A large app is split by feature — by business area: orders, customers, reports.
core/— things needed once: auth, interceptors, the page shell.shared/— shared UI components and pipes with no business logic.features/orders/— the routes, pages and state of one feature.
How to use
A feature is wired in as a lazy route: loadChildren: () => import('./orders/orders.routes'). Its code goes into a separate file (a chunk) and loads on first navigation.
Common mistake
Statically importing a lazy feature’s component somewhere else, for example in app.ts. It then lands in the main bundle, and lazy loading is gone.
Takeaway: a folder per feature and a lazy route per feature are the backbone of a large app.
When to choose what
| State approach | When |
|---|---|
| a service with signals | state of a single feature, simple operations |
| NgRx SignalStore | many stores, you want a uniform structure and plugins: withEntities, events |
| NgRx Store | state shared by many features, complex event flows, Redux DevTools |
Performance
- OnPush and signals — fewer checks.
trackbyidin@for— DOM nodes are reused, not recreated.- Long lists — virtual scrolling:
cdk-virtual-scroll-viewportfrom the Angular CDK orscrollable="virtual"in Kendo Grid. - Heavy widgets — charts, the scheduler — go inside
@defer.
Pitfalls
providedIn: 'root'for page state: it survives leaving the page, and stale data shows up when you come back. Declare such a store in route or componentproviders.- A global store for everything, including the state of a single form.
- A Kendo Grid with thousands of rows and no paging or virtual scrolling.
Rule of thumb
Start with a signal-based service. SignalStore when there are many stores and you want one shape; NgRx Store when the state is truly shared and complex.
Takeaway: feature-based structure, lazy routes and state kept next to the feature that owns it.
Trade-offs in state
- Most “state” is a cache of server data.
httpResourceor a service covers it; a global store is for what’s genuinely shared: the user, permissions, settings. - NgRx Store gives tracing and time travel in Redux DevTools at the cost of boilerplate: an action, a reducer, a selector and an effect per operation.
- Route
providersgive a store the lifetime of the feature — like a scope in ASP.NET Core, but for a section of the app.
The cost of bundle size
budgetsinangular.jsonfail the build when the initial bundle exceeds the limit.ng build --stats-jsonand the esbuild analyzer show what’s in the bundle.- Kendo UI and Angular Material add weight: import only the components you use, and put Chart and Scheduler behind
@defer.
Component libraries
- Their
peerDependencieslimit Angular upgrades — check them beforeng update. - Your own wrapper such as
app-data-gridover Kendo Grid keeps configuration and a future library swap in one place.
Auth in an SPA
A functional interceptor attaches the access token, and a canMatch guard hides routes the user can’t open. A guard is a UX convenience; the API enforces permissions. Where to keep tokens and when you need a BFF is covered in OAuth 2.0 and OpenID Connect.
How to check in production
Core Web Vitals from real users and Lighthouse show the result, and the Coverage tab in Chrome shows how much loaded JavaScript never ran.
Takeaway: scale is held by feature boundaries, lazy loading and a deliberate choice of where state lives.
import { Routes } from '@angular/router';import { inject } from '@angular/core';import { AuthService } from './core/auth.service';import { OrdersStore } from './features/orders/orders.store';
export const routes: Routes = [ { path: '', loadComponent: () => import('./features/home/home').then(m => m.Home) }, { path: 'orders', canMatch: [() => inject(AuthService).isLoggedIn()], // a convenience; the API checks permissions providers: [OrdersStore], // one store per feature, lives while the section is open // A separate chunk: loads on the first navigation to /orders loadChildren: () => import('./features/orders/orders.routes').then(m => m.ORDERS_ROUTES), },];import { computed, inject } from '@angular/core';import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals';import { Order, OrdersApi } from './orders.api';
export const OrdersStore = signalStore( withState({ orders: [] as Order[], filter: '', loading: false }), withComputed(({ orders, filter }) => ({ // Derived state is a plain computed visible: computed(() => orders().filter(o => o.customer.includes(filter()))), })), withMethods((store, api = inject(OrdersApi)) => ({ setFilter(filter: string) { patchState(store, { filter }); }, async load() { patchState(store, { loading: true }); patchState(store, { orders: await api.loadAll(), loading: false }); }, })),);- Where do you declare a page’s store? — In route or component
providers, so it goes away with the section. - When is NgRx Store justified? — Many features share complex state, and you need a strict one-way data flow and Redux DevTools.
- Why can an import from a lazy feature break lazy loading? — A static import pulls the module into the main bundle.
- Does a guard protect data? — No, it’s UX: the SPA’s code is available to the user, and the API checks permissions.
- How do you speed up a table with 10,000 rows? — Paging or virtual scrolling, OnPush and
trackbyid.