Angular 18 Signals: Yeni Reaktif API'ler ve Zone.js'siz Değişiklik Algılama
Angular 18 Signals için kapsamlı rehber: input(), model(), viewChild(), zoneless mod. Kod örnekleriyle pratik geçiş kılavuzu.

Angular 18, Signals'in stabilize edilmesiyle framework'ün evriminde bir dönüm noktasına işaret ediyor. Bu yeni reaktif primitif, Angular bileşenlerinin nasıl oluşturulduğunu temelden değiştiriyor, geleneksel dekoratörlere modern bir alternatif sunuyor ve Zone.js'siz değişiklik algılamaya giden yolu açıyor.
Angular 18'in sinyal tabanlı API'leri: input(), model(), viewChild() ve daha hafif, daha performanslı uygulamalar için zoneless yapılandırma.
Angular 18'de Signals'i Anlamak
Signals, Angular'da reaktiviteye yeni bir yaklaşım sunuyor. Zone.js değişiklik algılamasına dayanan @Input() gibi klasik dekoratörlerin aksine, Signals ince taneli ve açık reaktivite sağlıyor. Her Signal bir değeri kapsüller ve bu değer değiştiğinde tüketicileri otomatik olarak bilgilendirir.
Bu yaklaşım birçok avantaj getiriyor: hedeflenmiş güncellemeler sayesinde daha iyi performans, computed() ve effect() fonksiyonlarıyla doğal entegrasyon ve Angular'ın zoneless geleceğine hazırlık.
// Demonstration of fundamental Signal concepts
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<div class="counter-container">
<h2>Counter: {{ count() }}</h2>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+1</button>
<button (click)="decrement()">-1</button>
<button (click)="reset()">Reset</button>
</div>
`
})
export class CounterComponent {
// Writable signal - value can be modified
count = signal(0);
// Computed signal - automatically derived from count
// Only recalculates when count changes
doubleCount = computed(() => this.count() * 2);
constructor() {
// Effect - executed on every count change
// Useful for side effects (logs, API calls, etc.)
effect(() => {
console.log(`New counter value: ${this.count()}`);
});
}
increment() {
// update() allows modification based on previous value
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
reset() {
// set() directly replaces the value
this.count.set(0);
}
}Signals reaktif konteynerler olarak çalışır: signal() yazılabilir bir Signal oluşturur, computed() hesaplanmış değerler türetir ve effect() değişikliklere yanıt olarak eylemler yürütmeye olanak tanır.
input() ile Signal Inputs
input() fonksiyonu geleneksel @Input() dekoratörünün yerini alır. Salt okunur bir InputSignal döndürerek verilerin her zaman üst bileşenden alt bileşene, yanlışlıkla değiştirilme riski olmadan aktığını garanti eder.
// Component using signal inputs
import { Component, input, computed } from '@angular/core';
interface Book {
id: string;
title: string;
author: string;
price: number;
discountPercent?: number;
}
@Component({
selector: 'app-book-card',
standalone: true,
template: `
<article class="book-card">
<h3>{{ book().title }}</h3>
<p class="author">By {{ book().author }}</p>
@if (hasDiscount()) {
<p class="price">
<span class="original">\${{ book().price }}</span>
<span class="discounted">\${{ discountedPrice() }}</span>
</p>
} @else {
<p class="price">\${{ book().price }}</p>
}
@if (featured()) {
<span class="badge">Featured</span>
}
</article>
`
})
export class BookCardComponent {
// Required input - template won't compile without this prop
book = input.required<Book>();
// Optional input with default value
featured = input(false);
// Computed based on input - automatically recalculated
hasDiscount = computed(() => {
const discount = this.book().discountPercent;
return discount !== undefined && discount > 0;
});
// Discounted price calculation
discountedPrice = computed(() => {
const { price, discountPercent } = this.book();
if (!discountPercent) return price;
return (price * (100 - discountPercent) / 100).toFixed(2);
});
}Üst bileşen şablonunda kullanım benzer olmaya devam eder, ancak tip güvenliği ve Signal reaktivitesi ile:
// Parent component using book-card
import { Component, signal } from '@angular/core';
import { BookCardComponent } from './book-card.component';
@Component({
selector: 'app-book-list',
standalone: true,
imports: [BookCardComponent],
template: `
<div class="book-grid">
@for (book of books(); track book.id) {
<app-book-card
[book]="book"
[featured]="book.id === featuredBookId()"
/>
}
</div>
`
})
export class BookListComponent {
books = signal<Book[]>([
{ id: '1', title: 'Clean Code', author: 'Robert C. Martin', price: 35 },
{ id: '2', title: 'The Pragmatic Programmer', author: 'David Thomas', price: 42, discountPercent: 15 }
]);
featuredBookId = signal('1');
}@Input() ile temel fark: signal inputs salt okunurdur. Alt bileşenden this.book.set() çağrısı yapılamaz, bu da tek yönlü veri akışını güçlendirir.
model() ile Çift Yönlü Bağlama
Çift yönlü senkronizasyon gerektiren durumlar için Angular 18 model() fonksiyonunu sunar. Bu fonksiyon, değişiklikleri otomatik olarak üst bileşene yayan yazılabilir bir Signal oluşturur.
// Component with bidirectional binding via model()
import { Component, model, output, computed } from '@angular/core';
@Component({
selector: 'app-search-input',
standalone: true,
template: `
<div class="search-container">
<input
type="text"
[value]="query()"
(input)="onInput($event)"
[placeholder]="placeholder()"
class="search-input"
/>
@if (query().length > 0) {
<button (click)="clear()" class="clear-btn">×</button>
}
<span class="char-count">{{ charCount() }} characters</span>
</div>
`
})
export class SearchInputComponent {
// model() creates a bidirectional Signal
// Modifications propagate to parent
query = model('');
// Classic input for configuration
placeholder = model('Search...');
// Output for additional events
searchSubmitted = output<string>();
// Computed based on model
charCount = computed(() => this.query().length);
onInput(event: Event) {
const value = (event.target as HTMLInputElement).value;
// Update model - propagates to parent
this.query.set(value);
}
clear() {
this.query.set('');
}
submit() {
if (this.query().length > 0) {
this.searchSubmitted.emit(this.query());
}
}
}Üst bileşen, çift yönlü bağlama için banana-in-a-box sözdizimi olan [()] kullanır:
// Using two-way binding with model()
import { Component, signal, effect } from '@angular/core';
import { SearchInputComponent } from './search-input.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [SearchInputComponent],
template: `
<div class="app-container">
<app-search-input [(query)]="searchTerm" />
<p>Current search: {{ searchTerm() }}</p>
<div class="results">
@for (result of filteredResults(); track result.id) {
<div class="result-item">{{ result.name }}</div>
}
</div>
</div>
`
})
export class AppComponent {
// Local signal synchronized with child component
searchTerm = signal('');
results = signal([
{ id: 1, name: 'Angular 18' },
{ id: 2, name: 'React 19' },
{ id: 3, name: 'Vue 3' }
]);
// Reactive filtering based on searchTerm
filteredResults = computed(() => {
const term = this.searchTerm().toLowerCase();
if (!term) return this.results();
return this.results().filter(r =>
r.name.toLowerCase().includes(term)
);
});
}Salt okunur veriler (üst → alt) için input() kullanılır. Alt bileşenin değeri değiştirmesi gerektiğinde (çift yönlü) model() tercih edilir.
viewChild() ve contentChild() ile Signal Queries
viewChild(), viewChildren(), contentChild() ve contentChildren() fonksiyonları karşılık gelen dekoratörlerin yerini alır. Signals döndürerek ngAfterViewInit gibi yaşam döngüsü hook'larına olan ihtiyacı ortadan kaldırır.
// Demonstration of signal queries
import {
Component,
viewChild,
viewChildren,
ElementRef,
effect,
signal
} from '@angular/core';
import { FormFieldComponent } from './form-field.component';
@Component({
selector: 'app-form-container',
standalone: true,
imports: [FormFieldComponent],
template: `
<form #formElement (submit)="onSubmit($event)">
<input #firstInput type="text" placeholder="Name" />
<app-form-field label="Email" />
<app-form-field label="Phone" />
<div class="actions">
<button type="submit">Submit</button>
<button type="button" (click)="focusFirst()">Focus first field</button>
</div>
</form>
`
})
export class FormContainerComponent {
// viewChild returns Signal<ElementRef | undefined>
formElement = viewChild<ElementRef>('formElement');
// viewChild.required guarantees element exists
firstInput = viewChild.required<ElementRef<HTMLInputElement>>('firstInput');
// Query on a component - returns the component itself
firstFormField = viewChild(FormFieldComponent);
// viewChildren for multiple elements
allFormFields = viewChildren(FormFieldComponent);
constructor() {
// Effect replaces ngAfterViewInit for queries
effect(() => {
// Signal is automatically resolved
const input = this.firstInput();
console.log('First input available:', input.nativeElement);
});
// React to list changes
effect(() => {
const fields = this.allFormFields();
console.log(`${fields.length} form fields found`);
});
}
focusFirst() {
// Direct access via Signal
this.firstInput().nativeElement.focus();
}
onSubmit(event: Event) {
event.preventDefault();
// Access the form
const form = this.formElement();
if (form) {
console.log('Form submitted');
}
}
}İçerik yansıtma ve erişim için contentChild() benzer şekilde çalışır:
// Using contentChild for projected content
import { Component, contentChild, contentChildren, TemplateRef } from '@angular/core';
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<header class="card-header">
<ng-content select="[card-title]" />
</header>
<div class="card-body">
<ng-content />
</div>
@if (hasFooter()) {
<footer class="card-footer">
<ng-content select="[card-footer]" />
</footer>
}
</div>
`
})
export class CardComponent {
// Detect if footer was projected
footerContent = contentChild<ElementRef>('[card-footer]');
// Computed to check footer presence
hasFooter = computed(() => this.footerContent() !== undefined);
}Angular mülakatlarında başarılı olmaya hazır mısın?
İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.
Zone.js'siz Değişiklik Algılama (Zoneless)
Angular 18, deneysel modda Zone.js'siz değişiklik algılamayı sunuyor. Bu özellik, bundle boyutunu yaklaşık 13 KB azaltıyor ve tarayıcının asenkron API'lerine uygulanan monkey-patch'leri ortadan kaldırarak performansı artırıyor.
// Configuring the application in zoneless mode
import { bootstrapApplication } from '@angular/platform-browser';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
// Enable experimental zoneless detection
provideExperimentalZonelessChangeDetection()
]
});angular.json yapılandırmasının da Zone.js'yi kaldırmak için güncellenmesi gerekir:
{
"projects": {
"my-app": {
"architect": {
"build": {
"options": {
"polyfills": []
}
}
}
}
}
}Zoneless modda değişiklik algılama şu durumlarda otomatik olarak tetiklenir: Signal güncellemesi, markForCheck() çağrısı, AsyncPipe üzerinden yeni değer alımı veya bileşen ekleme/çıkarma.
// Component optimized for zoneless mode
import {
Component,
signal,
ChangeDetectionStrategy,
inject
} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-zoneless-counter',
standalone: true,
// OnPush recommended for zoneless
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="counter">
<p>Counter: {{ count() }}</p>
<button (click)="increment()">Increment</button>
@if (loading()) {
<p>Loading...</p>
}
@if (data()) {
<pre>{{ data() | json }}</pre>
}
</div>
`
})
export class ZonelessCounterComponent {
private http = inject(HttpClient);
count = signal(0);
loading = signal(false);
data = signal<any>(null);
increment() {
// Signal update triggers detection
this.count.update(c => c + 1);
}
async fetchData() {
this.loading.set(true);
try {
// Signals guarantee view updates
const response = await fetch('/api/data');
const json = await response.json();
this.data.set(json);
} finally {
this.loading.set(false);
}
}
}ChangeDetectionStrategy.OnPush ve Signals kullanan bileşenler genellikle zoneless modla uyumludur. Signal olmayan özelliklerin doğrudan değiştirilmesinden kaçınılmalıdır.
Mevcut Bileşenlerin Taşınması
Sinyal tabanlı API'lere geçiş kademeli olarak yapılabilir. Geleneksel bir bileşenin yeniden yapılandırma örneği:
// BEFORE: Component with classic decorators
// user-profile-legacy.component.ts
import { Component, Input, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-user-profile-legacy',
template: `
<div #container>
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
`
})
export class UserProfileLegacyComponent implements AfterViewInit {
@Input() user!: { name: string; email: string };
@ViewChild('container') container!: ElementRef;
ngAfterViewInit() {
console.log('Container ready:', this.container.nativeElement);
}
}// AFTER: Component migrated to Signals
// user-profile.component.ts
import { Component, input, viewChild, ElementRef, effect } from '@angular/core';
interface User {
name: string;
email: string;
}
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
<div #container>
<h2>{{ user().name }}</h2>
<p>{{ user().email }}</p>
</div>
`
})
export class UserProfileComponent {
// input.required replaces @Input() with !
user = input.required<User>();
// viewChild.required replaces @ViewChild with !
container = viewChild.required<ElementRef>('container');
constructor() {
// effect replaces ngAfterViewInit for queries
effect(() => {
console.log('Container ready:', this.container().nativeElement);
});
}
}Bu taşımanın avantajları: daha sıkı tip kontrolü, otomatik reaktivite, daha az şablon kodu ve zoneless mod uyumluluğu.
Signals ile En İyi Uygulamalar
Angular 18'de Signals'den en iyi şekilde yararlanmak için temel öneriler:
// Example of best practices with Signals
import {
Component,
signal,
computed,
effect,
untracked,
ChangeDetectionStrategy
} from '@angular/core';
interface Product {
id: string;
name: string;
price: number;
quantity: number;
}
@Component({
selector: 'app-cart',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="cart">
<h2>Cart ({{ itemCount() }} items)</h2>
@for (item of items(); track item.id) {
<div class="cart-item">
<span>{{ item.name }}</span>
<span>{{ item.quantity }} × \${{ item.price }}</span>
<button (click)="removeItem(item.id)">Remove</button>
</div>
}
<div class="cart-total">
<strong>Total: \${{ total() }}</strong>
</div>
</div>
`
})
export class CartComponent {
// Signal for mutable data
items = signal<Product[]>([]);
// Computed for derived values - avoids unnecessary recalculations
itemCount = computed(() => this.items().length);
total = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
constructor() {
// Effect for side effects (analytics, persistence)
effect(() => {
const currentItems = this.items();
// untracked avoids creating a dependency
untracked(() => {
localStorage.setItem('cart', JSON.stringify(currentItems));
});
});
}
addItem(product: Product) {
// update() for modifications based on previous state
this.items.update(current => {
const existing = current.find(i => i.id === product.id);
if (existing) {
return current.map(i =>
i.id === product.id
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...current, { ...product, quantity: 1 }];
});
}
removeItem(id: string) {
this.items.update(current => current.filter(i => i.id !== id));
}
}Akılda tutulması gereken temel noktalar:
- Türetilmiş değerler için şablonda yeniden hesaplama yerine
computed()kullanılmalıdır - Yeni değer eskisine bağlı olduğunda
set()yerineupdate()tercih edilmelidir - Effect'lerde döngüsel bağımlılıklardan kaçınmak için
untracked()kullanılmalıdır - Render optimizasyonu için
@fordöngülerindetrackher zaman belirtilmelidir
Sonuç
Angular 18, Signals aracılığıyla Zone.js'siz bir geleceğin temellerini atıyor. Temel çıkarımlar:
- input(), daha sıkı tip kontrolü ve garantili salt okunur erişim ile
@Input()dekoratörünün yerini alır - model(), üst ve alt bileşenler arasında reaktif çift yönlü bağlamayı mümkün kılar
- viewChild() ve contentChild(), yaşam döngüsü hook'larına olan ihtiyacı ortadan kaldırır
- Zoneless, bundle boyutunu azaltır ve performansı artırır
- computed() ve effect(), reaktif ekosistemi tamamlar
- Bileşen bileşen kademeli geçiş mümkündür
Signals'in benimsenmesi, Angular uygulamalarını zoneless modun norm haline geleceği gelecek sürümlere hazırlar. Bu dönüşüm, uzun vadeli bakım kolaylığı ve performans için akıllıca bir yatırımdır.
Pratik yapmaya başla!
Mülakat simülatörleri ve teknik testlerle bilgini test et.
Etiketler
Paylaş
