Angular Signals та Computed у 2026: Тонка Реактивність та Питання Співбесід
Опануйте Angular Signals, computed, effect та linkedSignal в Angular 20+. Патерни тонкої реактивності та підготовка до співбесід.

Angular Signals представляють найзначнішу зміну в моделі реактивності Angular з моменту створення фреймворку. Представлені в Angular 17 і тепер стандартний підхід в Angular 20+, сигнали забезпечують тонку реактивність, що усуває непотрібні цикли виявлення змін та безперешкодно інтегрується з беззонними (zoneless) застосунками.
Сигнали є синхронними, вільними від глітчів реактивними примітивами. Computed signal перераховується лише коли змінюються його залежності і лише раз за цикл змін, незалежно від кількості прочитань.
Розуміння Примітивів Signal в Angular 20
Система сигналів Angular складається з трьох основних примітивів: signal(), computed() та effect(). Кожен слугує певній меті в реактивному графі.
Записуваний сигнал зберігає значення, яке можна оновити за допомогою .set() або .update(). Читання сигналу повертає поточне значення, а всі computed signals або effects, що залежать від нього, отримують сповіщення про зміни.
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+1</button>
`
})
export class CounterComponent {
// Writable signal with initial value
count = signal(0);
// Computed signal derives from count
// Only recalculates when count changes
doubleCount = computed(() => this.count() * 2);
increment(): void {
// .update() receives current value as argument
this.count.update(c => c + 1);
}
}Ключова відмінність від RxJS BehaviorSubject: сигнали синхронні та вільні від глітчів. Коли count змінюється, doubleCount перераховується рівно один раз, навіть якщо читається кілька разів у тому ж синхронному блоці.
Computed Signals: Ледаче Обчислення та Мемоізація
Computed signals кешують свій результат до зміни залежності. Ця мемоізація автоматична, на відміну від RxJS, де shareReplay або distinctUntilChanged потрібно додавати вручну.
import { Component, signal, computed } from '@angular/core';
interface User {
id: number;
name: string;
purchases: number[];
}
@Component({
selector: 'app-user-stats',
standalone: true,
template: `
<div>
<p>User: {{ user().name }}</p>
<p>Total spent: {{ totalSpent() | currency }}</p>
<p>Average purchase: {{ averagePurchase() | currency }}</p>
</div>
`
})
export class UserStatsComponent {
user = signal<User>({
id: 1,
name: 'Alice',
purchases: [99.99, 149.50, 29.99]
});
// Computed tracks only the properties it reads
totalSpent = computed(() => {
const purchases = this.user().purchases;
return purchases.reduce((sum, p) => sum + p, 0);
});
// Depends on another computed signal
averagePurchase = computed(() => {
const total = this.totalSpent();
const count = this.user().purchases.length;
return count > 0 ? total / count : 0;
});
}Computed signal обчислюється ледаче: функція обчислення виконується лише при першому читанні сигналу, а не при оголошенні. Подальші читання повертають кешоване значення до інвалідації залежністю.
Effect: Побічні Ефекти в Реактивному Графі
Effects виконують код у відповідь на зміни сигналів. Вони запускаються асинхронно під час циклу виявлення змін, після завершення всіх синхронних оновлень.
import { Component, signal, effect, inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Component({
selector: 'app-theme-sync',
standalone: true,
template: `
<button (click)="toggleTheme()">Toggle Theme</button>
`
})
export class ThemeSyncComponent {
private document = inject(DOCUMENT);
isDarkMode = signal(false);
constructor() {
// Effect syncs signal state to DOM attribute
effect(() => {
const theme = this.isDarkMode() ? 'dark' : 'light';
this.document.documentElement.setAttribute('data-theme', theme);
});
// Effect for localStorage persistence
effect(() => {
localStorage.setItem('theme', this.isDarkMode() ? 'dark' : 'light');
});
}
toggleTheme(): void {
this.isDarkMode.update(dark => !dark);
}
}Effects відстежують залежності динамічно. Якщо умовна гілка не виконується, сигнали, що читаються лише в цій гілці, не відстежуються до зміни умови.
Використання effect для копіювання даних з одного сигналу в інший вказує на проблему проектування. Для похідного стану слід віддавати перевагу computed або linkedSignal, коли похідне значення має бути записуваним.
linkedSignal: Записуваний Похідний Стан
Представлений в Angular 19 і стабільний в Angular 20, linkedSignal створює записуваний сигнал, що скидається при зміні його джерела. Він вирішує сценарії, де computed є лише для читання, але похідне значення потребує локальної модифікації.
import { Component, signal, linkedSignal, computed } from '@angular/core';
@Component({
selector: 'app-pagination',
standalone: true,
template: `
<select (change)="pageSize.set(+$any($event.target).value)">
<option [value]="10">10 per page</option>
<option [value]="25">25 per page</option>
<option [value]="50">50 per page</option>
</select>
<p>Page {{ currentPage() }} of {{ totalPages() }}</p>
<button (click)="prevPage()" [disabled]="currentPage() === 1">Prev</button>
<button (click)="nextPage()" [disabled]="currentPage() === totalPages()">Next</button>
`
})
export class PaginationComponent {
totalItems = signal(243);
pageSize = signal(10);
totalPages = computed(() =>
Math.ceil(this.totalItems() / this.pageSize())
);
// linkedSignal resets to 1 when pageSize or totalItems changes
// but allows manual navigation via prevPage/nextPage
currentPage = linkedSignal(() => 1);
prevPage(): void {
this.currentPage.update(p => Math.max(1, p - 1));
}
nextPage(): void {
this.currentPage.update(p => Math.min(this.totalPages(), p + 1));
}
}Без linkedSignal цей патерн вимагав би або effect, що записує в інший сигнал (антипатерн), або складну оркестрацію RxJS.
Готовий до співбесід з Angular?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
API Компонентів на Основі Сигналів: input, model, viewChild
Angular 20 переводить API компонентів на основі сигналів у стабільний статус. Вони замінюють декоратори реактивними альтернативами, що інтегруються в граф сигналів.
import {
Component,
input,
model,
output,
computed,
viewChild,
ElementRef
} from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div #card class="card" [class.selected]="selected()">
<h3>{{ fullName() }}</h3>
<button (click)="toggle()">{{ selected() ? 'Deselect' : 'Select' }}</button>
</div>
`
})
export class UserCardComponent {
// Signal input replaces @Input()
firstName = input.required<string>();
lastName = input<string>('');
// model() creates two-way bindable signal
// Parent uses [(selected)]="parentSignal"
selected = model(false);
// Output emitter for events
cardClicked = output<void>();
// viewChild returns Signal<ElementRef | undefined>
cardElement = viewChild<ElementRef>('card');
// Computed derives from signal inputs
fullName = computed(() =>
`${this.firstName()} ${this.lastName()}`.trim()
);
toggle(): void {
this.selected.update(s => !s);
this.cardClicked.emit();
}
}Signal inputs забезпечують кращу типову безпеку: input.required() повертає InputSignal<T>, тоді як input() повертає InputSignal<T | undefined>, якщо не надано значення за замовчуванням.
Signals vs RxJS: Продуктивність Під Навантаженням
Бенчмарки в Angular 21+ показують, що сигнали зменшують накладні витрати виявлення змін у сценаріях з інтенсивним UI. При інтенсивних оновленнях сигнали продемонстрували чистіші профілі пам'яті та нижче використання CPU порівняно з прив'язками на основі RxJS.
Розподіл відповідальності чіткий:
| Випадок Використання | Рекомендований Підхід |
|---|---|
| Локальний стан компонента | signal() |
| Похідні значення UI | computed() |
| Синхронізація із зовнішніми API | effect() |
| HTTP запити | RxJS Observable + toSignal() |
| Потоки WebSocket | RxJS з операторами |
| Debounced ввід користувача | RxJS debounceTime, потім toSignal() |
| Складна async оркестрація | Pipeline RxJS |
RxJS залишається інструментом для асинхронних потоків, скасування та backpressure. Сигнали обробляють синхронний стан.
Інтероперабельність з RxJS: toSignal та toObservable
Angular надає утиліти в @angular/core/rxjs-interop для з'єднання сигналів та observables.
import { Component, signal, inject } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, switchMap, distinctUntilChanged } from 'rxjs';
interface SearchResult {
id: number;
title: string;
}
@Component({
selector: 'app-search',
standalone: true,
template: `
<input
[value]="query()"
(input)="query.set($any($event.target).value)"
placeholder="Search..."
/>
@if (results(); as items) {
<ul>
@for (item of items; track item.id) {
<li>{{ item.title }}</li>
}
</ul>
}
`
})
export class SearchComponent {
private http = inject(HttpClient);
query = signal('');
// Convert signal to observable for RxJS operators
private query$ = toObservable(this.query);
// RxJS pipeline with debounce, then back to signal
results = toSignal(
this.query$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(q => q.length > 2
? this.http.get<SearchResult[]>(`/api/search?q=${q}`)
: []
)
),
{ initialValue: [] as SearchResult[] }
);
}toSignal підписується при створенні та відписується при знищенні компонента. Опція initialValue усуває тип undefined з сигналу.
Питання Співбесід: Angular Signals
Технічні співбесіди у 2026 році часто перевіряють розуміння сигналів. Ось патерни, що відрізняють senior кандидатів.
П: Що відбувається, коли computed signal кидає помилку?
Помилка поширюється на кожен effect або computed, що його читає. Angular не кешує помилки — обчислення повторюється при наступному читанні. Продакшн код має обробляти помилки всередині функції computed.
П: Чи можна записувати в сигнал всередині computed?
Ні. Спроба викликати .set() або .update() всередині computed кидає помилку. Функції computed мають бути чистими. Слід використовувати linkedSignal, якщо потрібне похідне значення, яке також записуване.
П: Як effects обробляють читання сигналів в умовах?
// Dynamic dependency tracking example
const showDetails = signal(false);
const userDetails = signal({ name: 'Alice', email: 'alice@example.com' });
effect(() => {
if (showDetails()) {
// userDetails is only tracked when showDetails is true
console.log('Details:', userDetails().email);
}
});Effects відстежують залежності на основі останнього виконання. Якщо showDetails є false, зміни в userDetails не викликають effect.
П: Як працює рівність сигналів?
За замовчуванням сигнали використовують Object.is для порівняння рівності. Для об'єктів це означає рівність за посиланням. Можна надати власну функцію рівності:
const user = signal(
{ id: 1, name: 'Alice' },
{ equal: (a, b) => a.id === b.id }
);Кандидати, які згадують, що computed signals мемоізовані та ледаче обчислювані, демонструють розуміння, що виходить за межі базового використання API. Обговорення того, коли використовувати linkedSignal vs computed, показує знайомство з патернами Angular 19+.
Виявлення Змін: Сигнали та Беззонний Angular
Angular 22 за замовчуванням використовує беззонне виявлення змін для нових проектів. Сигнали є центральними для цього: вони повідомляють Angular точно, коли змінюється стан, усуваючи потребу Zone.js патчити асинхронні API.
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-zoneless-counter',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+1</button>
`
})
export class ZonelessCounterComponent {
count = signal(0);
increment(): void {
// Signal update triggers change detection
// No Zone.js required
this.count.update(c => c + 1);
}
}З сигналами Angular відстежує точно, які компоненти залежать від якого стану. Оновлення сигналу планує виявлення змін лише для уражених компонентів, а не всього дерева.
Resource API: Асинхронні Дані як Сигнали
Resource API Angular 20 (resource() та rxResource()) декларативно завантажує асинхронні дані в сигнали, замінюючи патерни ручної підписки.
import { Component, signal, computed } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
@if (userResource.isLoading()) {
<p>Loading...</p>
}
@if (userResource.error()) {
<p>Error: {{ userResource.error() }}</p>
}
@if (userResource.value(); as user) {
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
}
`
})
export class UserProfileComponent {
private http = inject(HttpClient);
userId = signal(1);
// Declarative async resource
// Refetches when userId changes
userResource = rxResource({
request: () => this.userId(),
loader: ({ request: id }) =>
this.http.get<User>(`/api/users/${id}`)
});
loadUser(id: number): void {
this.userId.set(id);
}
}Resource автоматично відстежує стан завантаження, помилки та розв'язане значення. Він повторно отримує дані при зміні сигналу request.
Найкращі Практики Архітектури Сигналів
Структурування сигналів у великих застосунках вимагає дисципліни. Ці патерни масштабуються від компонентів до сервісів.
Колокація пов'язаних сигналів: Групування сигналів, що змінюються разом. Компонент форми тримає сигнали своїх полів разом, а не розкиданими по сервісах.
Переміщення спільного стану до сервісів: Коли кільком компонентам потрібен той самий стан, сигнал слід перемістити до injectable сервісу:
import { Injectable, signal, computed } from '@angular/core';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
// Expose read-only signal to consumers
readonly cartItems = this.items.asReadonly();
readonly totalPrice = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
readonly itemCount = computed(() =>
this.items().reduce((sum, item) => sum + item.quantity, 0)
);
addItem(item: Omit<CartItem, 'quantity'>): void {
this.items.update(items => {
const existing = items.find(i => i.id === item.id);
if (existing) {
return items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...items, { ...item, quantity: 1 }];
});
}
}Перевага computed над effect: Якщо значення може бути похідним, слід використовувати computed. Effects призначені для побічних ефектів, що виходять за межі реактивного графа: маніпуляції DOM, localStorage, аналітика, мережеві запити.
Міграція з RxJS до Сигналів
Старі кодові бази Angular можуть мігрувати поступово. Утиліти інтероперабельності RxJS дозволяють сигналам та observables співіснувати.
// Before: RxJS-based
@Component({ ... })
export class LegacyComponent {
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
userName$ = this.user$.pipe(
map(u => u?.name ?? 'Guest')
);
}
// After: Signal-based
@Component({ ... })
export class ModernComponent {
user = signal<User | null>(null);
userName = computed(() => this.user()?.name ?? 'Guest');
}Для асинхронних операцій слід зберегти RxJS для pipeline та конвертувати на межі:
// Hybrid approach
private search$ = toObservable(this.searchQuery).pipe(
debounceTime(300),
switchMap(q => this.api.search(q))
);
results = toSignal(this.search$, { initialValue: [] });Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Підготовка до Питань Співбесід про Angular Signals
- Сигнали є синхронними примітивами; RxJS обробляє асинхронні потоки
- Computed signals є ледачими та мемоізованими, перераховуються лише при зміні залежностей
- Effects запускаються асинхронно під час виявлення змін, не одразу
- linkedSignal вирішує проблему записуваного похідного стану без effects
- Signal inputs (
input(),model()) замінюють декоратори реактивними альтернативами toSignalтаtoObservableз'єднують сигнали та RxJS для поступової міграції- Беззонний Angular покладається на сигнали для тонкого виявлення змін
- Resource API (
rxResource) декларативно керує асинхронним станом - Офіційний посібник Angular signals залишається авторитетним джерелом інформації про деталі API та найкращі практики
Чи знайдеш ти помилку в Angular?
Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Автор:
Anthony Fillion-MailletЗасновник SharpSkill
Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.
Оновлено 17 вересня 2026 р.
Поділитися
Пов'язані статті

Просунутий Dependency Injection в Angular 2026: Providers, Tokens та Питання на Співбесідах
Комплексний посібник з просунутих механізмів Dependency Injection в Angular. Providers, InjectionToken, ієрархія інжекторів та найпоширеніші питання на технічних співбесідах.

Синтаксис Control Flow в Angular 2026: @if, @for, @switch та питання на співбесідах
Повний посібник з блокового синтаксису керування потоком в Angular. Вивчіть @if, @for, @switch з практичними прикладами коду та підготуйтесь до технічних співбесід.

Angular HttpClient та Interceptor'и у 2026: Обробка HTTP-запитів та Питання на Співбесідах
Повний посібник з функціональних interceptor'ів Angular 20, від впровадження токенів автентифікації до логіки повторних спроб з експоненціальною затримкою. Містить питання для співбесід та production-патерни.