Mert Özen Her Satırda Daha İleri
Frontend Angular — #27

Angular 18 Yenilikleri: Zoneless Change Detection, @let ve Stable Control Flow

Mert Özen 31 Eki 2026 7 dk 3 görüntülenme
Angular 18 Yenilikleri: Zoneless Change Detection, @let ve Stable Control Flow

Angular 18 ile gelen deneysel zoneless change detection'ı, stable olan control flow ve @defer'i, 18.1'deki @let'i, ng-content varsayılan içeriğini, form event'lerini ve redirect fonksiyonlarını anlatıyoruz.

Sürüm notu: Bu yazı Angular 18 ile gelen yenilikleri anlatır; kodlar Angular 18 içindir.

Angular 18'de Ne Var

22 Mayıs 2024'te çıkan Angular 18, son iki sürümde attığımız tohumların ilk meyvelerini veriyor. Signal'lar component'in her köşesine yerleşmişti. Şimdi Angular bu bilgiyi kullanarak, deneysel olarak da olsa, Zone.js olmadan çalışabiliyor. Bir önceki sürümde developer preview olarak tanıştığımız control flow ve @defer stable oldu. 10 Temmuz 2024'te gelen 18.1 de template'lere @let ile yerel değişken tanımlamayı ekledi.

Bunların yanında daha küçük ama günlük hayatta işe yarayan üç değişikliğe bakacağız: ng-content için varsayılan içerik, formlarda tek bir event stream'i ve fonksiyon olarak yazılabilen route yönlendirmeleri.

Angular 12'de Ekranı Kim Güncelliyordu

Daha önce gördüğümüz change detection konusunu hatırla. Şu kodda ekranı güncellemek için hiçbir şey yapmıyoruz, ama iki saniye sonra mesaj yine de görünüyor:

@Component({
  selector: 'app-notification',
  template: `<p>{{ message }}</p>`
})
export class NotificationComponent implements OnInit {
  message = '';

  ngOnInit(): void {
    setTimeout(() => {
      this.message = 'Yeni bir mesajın var';
    }, 2000);
  }
}

Bunu sağlayan Zone.js. Uygulama açılırken tarayıcının setTimeout, addEventListener, XHR gibi API'lerini sarmalıyor ve her biri bittiğinde Angular'a "bir şey olmuş olabilir, bir bak" diyor. Angular da daha önce gördüğümüz gibi ağacı dolaşıp kontrol ediyor. Rahat, ama bedeli var: ekstra bir kütüphane yükleniyor, tarayıcı API'leri yamanıyor, hata stack trace'leri Zone.js çerçeveleriyle doluyor ve Angular aslında neyin değiştiğini hiç bilmeden tahmin yürütüyor.

Zoneless: Zone.js Olmadan Change Detection

Angular 18 ile Zone.js'i tamamen devreden çıkarmayı deneyebiliyorsun. Standalone bir projede app.config.ts'i şöyle değiştiriyorsun:

import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideExperimentalZonelessChangeDetection(),
    provideRouter(routes)
  ]
};

Angular 18 ile oluşturulan yeni projelerde bu dosyada provideZoneChangeDetection({ eventCoalescing: true }) satırı hazır geliyor; onu yukarıdaki provider ile değiştiriyorsun. Ardından angular.json'da build hedefinin polyfills dizisinden "zone.js"'i çıkarıyorsun. Böylece paket uygulamaya hiç yüklenmiyor.

Fonksiyonun adındaki "Experimental" kelimesi süs değil. Zoneless bu sürümde deneysel; API'nin adı ve davranışı ilerleyen sürümlerde değişebilir. Production'a almadan önce bunu hesaba kat.

Peki Zone.js yoksa Angular ne zaman ekranı güncelleyeceğini nereden biliyor? Artık tahmin etmiyor, haber bekliyor. Template'te okunan bir signal'ın değeri değiştiğinde, template'teki ya da host'taki bir event listener çalıştığında, async pipe yeni bir değer aldığında, markForCheck() çağrıldığında ya da bir view eklenip kaldırıldığında change detection planlanıyor. Bir zil sistemi gibi: görevli artık kapı kapı dolaşmıyor, hangi daire zili çalarsa oraya gidiyor.

Zoneless'ta Neye Dikkat Etmeli

Yukarıdaki Angular 12 örneğini zoneless bir uygulamada çalıştırırsan mesaj ekrana hiç gelmez. setTimeout artık kimseye haber vermiyor, düz bir property'nin değişmesi de Angular için görünmez. Çözüm, değişikliği bir signal üzerinden yapmak:

import { Component, OnInit, signal } from '@angular/core';

@Component({
  selector: 'app-notification',
  standalone: true,
  template: `<p>{{ message() }}</p>`
})
export class NotificationComponent implements OnInit {
  message = signal('');

  ngOnInit(): void {
    setTimeout(() => {
      this.message.set('Yeni bir mesajın var');
    }, 2000);
  }
}

set çağrıldığı anda Angular hangi component'in bu signal'ı okuduğunu biliyor ve bir change detection planlıyor. Bir tuzak var: aynı anda başka bir signal güncellendiği için düz bir property'nin de tesadüfen ekrana yansıdığını görebilirsin. Bu tesadüfe güvenme. Zoneless'a geçmeyi düşünüyorsan ekranda görünen state'i signal'a, async pipe'a ya da OnPush ile uyumlu kalıplara taşımak gerekiyor. Daha önce gördüğümüz OnPush kurallarına uyan bir kod tabanı bu geçişi çok daha rahat yapıyor.

Zone.js'i bırakmasan bile Angular 18'de bir değişiklik var. Yeni bir hibrit planlayıcı varsayılan olarak açık: Zone.js'li bir uygulamada da bir signal değiştiğinde ya da markForCheck() çağrıldığında, bu iş Zone.js'in dışında olsa bile change detection planlanıyor. Yani daha önce gördüğümüz runOutsideAngular içinde bir signal güncellersen ekran yine de yenileniyor.

Control Flow ve @defer Artık Stable

Angular 17'de developer preview olarak gelen @if, @for, @switch ve @defer Angular 18'de stable oldu. Sözdiziminde bir değişiklik yok; değişen, Angular ekibinin artık bu API'ler için kırıcı değişiklik yapmayacağını taahhüt etmesi.

Pratikte bu, ekiplerin "biraz daha bekleyelim" dediği noktanın geride kaldığı anlamına geliyor. Daha önce gördüğümüz control flow şematiğiyle mevcut template'lerini çevirmenin tam zamanı. *ngIf ve *ngFor ise bu sürümde hâlâ çalışıyor ve deprecated değil; ama yeni yazdığın kodda onları seçmek için bir sebep kalmadı.

@let ile Template Değişkeni

Angular 12'de bir observable'ın değerini template'te birkaç yerde kullanmak için şu numarayı yapardık:

<ng-container *ngIf="user$ | async as user">
  <h2>{{ user.firstName }} {{ user.lastName }}</h2>
  <p>{{ user.address.city }}, {{ user.address.country }}</p>
</ng-container>

*ngIf burada aslında koşul için değil, değişken tanımlamak için kullanılıyor. Değer 0 ya da boş string gibi falsy bir şey olduğunda blok tamamen kayboluyor. Angular 18.1 ile:

@let user = user$ | async;

@if (user) {
  @let address = user.address;
  <h2>{{ user.firstName }} {{ user.lastName }}</h2>
  <p>{{ address.city }}, {{ address.country }}</p>
}

@for (item of cartItems; track item.id) {
  @let lineTotal = item.price * item.quantity;
  <p>
    {{ item.name }}: {{ lineTotal }} TL
    @if (lineTotal > 1000) { <span>(kargo bedava)</span> }
  </p>
}

@let, template içinde bir ifadeye isim veriyor. İfade değiştikçe değişkenin değeri de kendiliğinden güncelleniyor. Bir kuralı var: template'ten yeniden atanamıyor, yani (click)="lineTotal = 0" gibi bir şey yazamazsın. Değişken tanımlandığı view'da ve onun içindeki bloklarda görünür; dışarıdan erişilemez ve tanımlandığı satırdan önce okunamaz. Mutfaktaki bir kaba etiket yapıştırmak gibi düşün: içindekini değiştirmiyorsun, sadece her seferinde "şu tezgâhın soldan üçüncü kabı" demek yerine adıyla çağırıyorsun.

Durum notu: @let 18.1'de developer preview olarak geldi.

ng-content İçin Varsayılan İçerik

Daha önce gördüğümüz content projection'da bir slot'a içerik gelmezse orası boş kalırdı:

<div class="card">
  <h3><ng-content select="[card-title]"></ng-content></h3>
  <ng-content></ng-content>
</div>

Varsayılan bir başlık göstermek için ek bir input açmak ya da @ContentChild ile içerik var mı diye kontrol etmek gerekiyordu. Angular 18'de ng-content etiketinin içine yazdığın şey, o slot boş kaldığında gösteriliyor:

<div class="card">
  <h3><ng-content select="[card-title]">Başlıksız kart</ng-content></h3>
  <ng-content>Bu kartta henüz içerik yok.</ng-content>
</div>

<app-card><p>Sadece gövde</p></app-card> yazarsan paragraf gövdeye yerleşiyor, başlıkta da "Başlıksız kart" görünüyor.

Formlar ve Router'dan İki Yenilik

Daha önce gördüğümüz reactive forms'ta değer ve geçerlilik için ayrı ayrı valueChanges ve statusChanges dinliyorduk; bir alana dokunulduğunu ya da alanın kirlendiğini haber veren bir observable ise yoktu. Angular 18'de her form control'ün tek bir events stream'i var:

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, StatusChangeEvent, TouchedChangeEvent, ValueChangeEvent, Validators } from '@angular/forms';

export class NewsletterComponent {
  email = new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.email] });

  constructor() {
    this.email.events.pipe(takeUntilDestroyed()).subscribe(event => {
      if (event instanceof ValueChangeEvent) {
        console.log('Değer:', event.value);
      }
      if (event instanceof StatusChangeEvent) {
        console.log('Durum:', event.status);
      }
      if (event instanceof TouchedChangeEvent) {
        console.log('Dokunuldu mu:', event.touched);
      }
    });
  }
}

Router tarafında redirectTo artık sadece sabit bir string değil, bir fonksiyon da olabiliyor. Angular 12'de redirectTo: '/products/:id' gibi sabit bir hedef verebiliyorduk; karar vermek gerekiyorsa bir guard yazıyorduk. Angular 18'de:

export const routes: Routes = [
  {
    path: 'legacy-profile',
    redirectTo: ({ queryParams }) => {
      const auth = inject(AuthService);
      const userId = queryParams['userId'];
      if (userId) {
        return `/users/${userId}`;
      }
      return auth.isLoggedIn() ? '/me' : '/login';
    }
  }
];

Fonksiyon route'un parametrelerini, query parametrelerini ve data'sını alıyor, bir string ya da UrlTree döndürüyor. İçinde daha önce gördüğümüz inject()'i kullanabiliyorsun.

Angular 18'e Geçiş: ng update

17'deki bir projeyi taşımak için:

ng update @angular/core@18 @angular/cli@18

Angular 18, Node.js ^18.19.1, ^20.11.1 ya da ^22.0.0 istiyor. TypeScript tarafında 18.0 sadece 5.4 ile çalışıyor, 18.1 ve sonrası 5.5'i de destekliyor. Güncelleme sırasında esbuild tabanlı builder'ları yeni @angular/build paketine taşıyan opsiyonel bir migration da teklif ediliyor; webpack tabanlı builder kullanıyorsan o adım bir şey yapmıyor.

Bir deprecation'a da dikkat: HttpClientModule artık deprecated. Daha önce gördüğümüz provideHttpClient() onun yerini alıyor. Yeni projelerde statik dosyalar için src/assets/ yerine kökte bir public/ klasörü geliyor; mevcut projen olduğu gibi çalışmaya devam ediyor.

Küçük Bir Deneme

Angular 18 ile yeni bir proje oluştur. app.config.ts'teki provideZoneChangeDetection satırını provideExperimentalZonelessChangeDetection() ile değiştir ve angular.json'daki build polyfill'lerinden zone.js'i çıkar. Ardından yukarıdaki bildirim component'inin düz property'li hâlini yaz ve mesajın hiç gelmediğini gör. Sonra signal'lı hâline geç. Son olarak template'e bir sepet listesi ekle ve satır toplamlarını @let ile hesapla.

Bu yazıda bile her component'e hâlâ standalone: true yazdık. Bir sonraki yazıda Angular 19 ile bu satırın neden gereksiz hâle geldiğini, başka bir signal'a bağlı ama elle de değiştirilebilen linkedSignal'ı ve asenkron veriyi signal dünyasına taşıyan deneysel resource() API'sini göreceğiz.