# 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: `
@if (query().length > 0) { } {{ charCount() }} characters
` }) 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: `

{{ user.name }}

{{ user.email }}

` }) export class UserProfileLegacyComponent implements AfterViewInit { @Input() user!: { name: string; email: string }; @ViewChild('container') container!: ElementRef; ngAfterViewInit() { console.log('Container ready:', this.container.nativeElement); } } ``` ```typescript // 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: `

{{ user().name }}

{{ user().email }}

` }) 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: `

Cart ({{ itemCount() }} items)

@for (item of items(); track item.id) {
{{ item.name }} {{ item.quantity }} × \${{ item.price }}
}
Total: \${{ total() }}
` }) 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