# 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.
- Published: 2026-01-18
- Updated: 2026-04-10
- Author: SharpSkill
- Tags: angular 18, angular signals, zoneless, signal inputs, reactivity
- Reading time: 12 min
---
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.
> **Neler öğrenilecek**
>
> 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.
```typescript
// signals-basics.component.ts
// Demonstration of fundamental Signal concepts
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
Counter: {{ count() }}
Double: {{ doubleCount() }}
`
})
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.
```typescript
// book-card.component.ts
// 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: `
{{ book().title }}
By {{ book().author }}
@if (hasDiscount()) {
\${{ book().price }}\${{ discountedPrice() }}
} @else {
\${{ book().price }}
}
@if (featured()) {
Featured
}
`
})
export class BookCardComponent {
// Required input - template won't compile without this prop
book = input.required();
// 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:
```typescript
// book-list.component.ts
// 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: `
@for (book of books(); track book.id) {
}
`
})
export class BookListComponent {
books = signal([
{ 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.
```typescript
// search-input.component.ts
// Component with bidirectional binding via model()
import { Component, model, output, computed } from '@angular/core';
@Component({
selector: 'app-search-input',
standalone: true,
template: `
`
})
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();
// 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:
```typescript
// app.component.ts
// 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: `
Current search: {{ searchTerm() }}
@for (result of filteredResults(); track result.id) {
{{ result.name }}
}
`
})
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)
);
});
}
```
> **model() vs input()**
>
> 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.
```typescript
// form-container.component.ts
// 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: `
`
})
export class FormContainerComponent {
// viewChild returns Signal
formElement = viewChild('formElement');
// viewChild.required guarantees element exists
firstInput = viewChild.required>('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:
```typescript
// card.component.ts
// Using contentChild for projected content
import { Component, contentChild, contentChildren, TemplateRef } from '@angular/core';
@Component({
selector: 'app-card',
standalone: true,
template: `
@if (hasFooter()) {
}
`
})
export class CardComponent {
// Detect if footer was projected
footerContent = contentChild('[card-footer]');
// Computed to check footer presence
hasFooter = computed(() => this.footerContent() !== undefined);
}
```
## 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.
```typescript
// main.ts
// 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:
```json
{
"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.
```typescript
// zoneless-counter.component.ts
// 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: `
Counter: {{ count() }}
@if (loading()) {
Loading...
}
@if (data()) {
{{ data() | json }}
}
`
})
export class ZonelessCounterComponent {
private http = inject(HttpClient);
count = signal(0);
loading = signal(false);
data = signal(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);
}
}
}
```
> **Zoneless Uyumluluk**
>
> `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:
```typescript
// 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: `
`
})
export class UserProfileComponent {
// input.required replaces @Input() with !
user = input.required();
// viewChild.required replaces @ViewChild with !
container = viewChild.required('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:
```typescript
// best-practices.component.ts
// 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: `
`
})
export class CartComponent {
// Signal for mutable data
items = signal([]);
// 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()` yerine `update()` tercih edilmelidir
- Effect'lerde döngüsel bağımlılıklardan kaçınmak için `untracked()` kullanılmalıdır
- Render optimizasyonu için `@for` döngülerinde `track` her 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.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/tr/blog/angular/angular-18-signals-new-features