Angular 21: Varsayılan Zoneless, Deneysel Signal Forms ve Vitest
Angular 21'de yeni projeler Zone.js'siz doğuyor, Signal Forms deneysel olarak geliyor ve 21.1'de [formField] adını alıyor, Vitest varsayılan test runner oluyor. Angular 12 ile yan yana bakıyoruz.
Sürüm notu: Bu yazı Angular 21 ile gelen yenilikleri anlatır; kodlar Angular 21 içindir.
Varsayılanların Değiştiği Sürüm
Angular 20'de signal API'lerinin stable olduğunu, httpResource'u ve app.ts ile başlayan yeni dosya adlandırmasını gördük. Zoneless da o sürümün içinde stable olmuştu ama hâlâ bir tercihti. 19 Kasım 2025'te çıkan Angular 21, uzun süredir hazırlanan birkaç şeyi varsayılan hâline getiriyor.
Yeni bir proje artık Zone.js olmadan doğuyor. Formlar için signal tabanlı yepyeni bir yaklaşım deneysel olarak geliyor. Testler Karma ile değil Vitest ile koşuyor. Bunlara ek olarak erişilebilirlik için yeni bir paket ve bir sonraki sürümün habercisi olan küçük bir deprecation var.
Angular 12'de Zone.js Her Yerdeydi
Daha önce gördüğümüz change detection konusunda Zone.js'in tarayıcıdaki event'leri, timer'ları ve HTTP isteklerini nasıl yakaladığını konuşmuştuk. Angular 12 projesinde bunun kaynağı src/polyfills.ts içindeki tek bir satırdı:
import 'zone.js'; // Included with Angular CLI.
Bu satır uygulama açılmadan önce tarayıcının setTimeout, addEventListener ve XMLHttpRequest gibi API'lerini sarıyordu. Böylece Angular "bir şey olmuş olabilir" diye anlıyor ve bütün component ağacını kontrol ediyordu. Angular 21 ile ng new komutunun ürettiği app.config.ts şöyle:
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes)
]
};
Angular 20'de zoneless için eklememiz gereken provideZonelessChangeDetection() da burada yok. Gerek kalmadı, çünkü Angular 21'de zoneless varsayılan davranış. package.json içinde zone.js bağımlılığı da yok.
Zone.js'e dayanan mevcut bir uygulama için durum farklı. Böyle bir uygulama artık bunu açıkça söylemek zorunda: kök provider'lara provideZoneChangeDetection() ekleniyor. ng update sırasında çalışan migration, gerekiyorsa bunu senin yerine yapıyor. Yani yükseltme yaptın diye uygulaman bir gecede zoneless olmuyor.
Zoneless'ta Ekran Ne Zaman Güncellenir?
Zone.js gidince akla ilk gelen soru bu. Şu component'e bak:
@Component({
selector: 'app-order-status',
template: `<p>{{ status }}</p>`
})
export class OrderStatus {
status = 'Hazırlanıyor';
constructor() {
setTimeout(() => this.status = 'Kargoya verildi', 2000);
}
}
Angular 12'de iki saniye sonra ekran kendiliğinden güncellenirdi, çünkü Zone.js setTimeout'un bittiğini fark ederdi. Zoneless bir uygulamada ise ekranda "Hazırlanıyor" yazısı kalır. Property değişti ama Angular'a bunu haber veren kimse yok.
export class OrderStatus {
protected readonly status = signal('Hazırlanıyor');
constructor() {
setTimeout(() => this.status.set('Kargoya verildi'), 2000);
}
}
Template'te {{ status() }} yazdığında sorun çözülüyor. Template'in okuduğu bir signal değiştiğinde Angular bunu biliyor ve sadece gereken yeri yeniliyor. Zoneless dünyada Angular'ı harekete geçiren şeyler belli: template'in okuduğu signal'ların değişmesi, template'teki (click) gibi event listener'lar, async pipe ve markForCheck() çağrıları.
Bir apartman görevlisi düşün. Zone.js'li dünyada görevli her kapı sesinde bütün katları tek tek dolaşıyordu. Zoneless'ta ise sadece zili çalan dairenin kapısını çalıyor.
Signal Forms: Formlara Yeni Bir Bakış
Angular 12'de bir kayıt formunu reactive forms ile şöyle kurardık:
export class RegisterComponent {
registerForm = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]),
password: new FormControl('', [Validators.required, Validators.minLength(8)])
});
}
<form [formGroup]="registerForm">
<input type="email" formControlName="email">
<p *ngIf="registerForm.get('email')?.touched && registerForm.get('email')?.invalid">
Geçerli bir e-posta gir
</p>
<input type="password" formControlName="password">
</form>
Veri form nesnesinin içinde yaşıyordu, değerler any tipindeydi ve alanlara get('email') gibi string anahtarlarla ulaşıyorduk. Angular 21'de deneysel olarak gelen Signal Forms ile aynı form:
import { Component, signal } from '@angular/core';
import { form, Field, required, email, minLength } from '@angular/forms/signals';
@Component({
selector: 'app-register',
imports: [Field],
template: `
<input type="email" [field]="registerForm.email" />
@if (registerForm.email().touched() && !registerForm.email().valid()) {
@for (error of registerForm.email().errors(); track error.kind) {
<p>{{ error.message }}</p>
}
}
<input type="password" [field]="registerForm.password" />
<p>Model: {{ account().email }}</p>
`
})
export class Register {
protected readonly account = signal({ email: '', password: '' });
protected readonly registerForm = form(this.account, (path) => {
required(path.email, { message: 'E-posta zorunlu' });
email(path.email, { message: 'Geçerli bir e-posta gir' });
minLength(path.password, 8, { message: 'Şifre en az 8 karakter olmalı' });
});
}
Başrolde düz bir signal var: account. Verinin kendisi orada duruyor. form() bu signal'ın şeklini okuyup her alan için bir karşılık üretiyor; registerForm.email diye yazdığında TypeScript bu alanın var olduğunu biliyor. İkinci parametredeki fonksiyon da validator'ları tanımlıyor ve mesajları doğrudan validator'ın yanına yazıyorsun.
Template'teki [field] directive'i input'u alana bağlıyor. Bağ iki yönlü: kullanıcı yazdıkça account signal'ı güncelleniyor, son satırdaki "Model" paragrafında bunu canlı görebilirsin. Bir alanın durumuna ulaşmak için onu fonksiyon gibi çağırıyorsun: registerForm.email() sana touched(), valid(), errors() gibi signal'lar veriyor.
Reactive forms'ta form, verinin bir kopyasını tutan ayrı bir kutuydu. Signal Forms'ta ise form, verinin üstüne giydirilmiş bir kılıf gibi. Bu API Angular 21'de experimental; şekli değişebilir ve nitekim çok geçmeden değişti.
21.1'de İsim Değişikliği: [formField]
21.1 sürümüyle [field] directive'inin adı değişti:
import { form, FormField, required, email } from '@angular/forms/signals';
@Component({
selector: 'app-register',
imports: [FormField],
template: `<input type="email" [formField]="registerForm.email" />`
})
export class Register { /* ... */ }
Hem class (Field yerine FormField) hem selector ([field] yerine [formField]) değişti, eski isim de kaldırıldı. Sebep basit: field o kadar genel bir kelime ki projelerdeki başka component'lerle çakışma ihtimali yüksekti. 21.0 ile yazdığın bir formu 21.1'e taşırken bu satırları güncellemen gerekiyor. Experimental bir API kullanmanın bedeli tam olarak bu.
Karma'dan Vitest'e
Angular 12'de ng test dediğinde Karma bir Chrome penceresi açar, testleri onun içinde koştururdu. Projede bunun için karma.conf.js ve src/test.ts dosyaları vardı, angular.json içindeki builder da @angular-devkit/build-angular:karma idi. Angular 21'de yeni bir projenin test ayarı tek satır:
"test": {
"builder": "@angular/build:unit-test"
}
Bu builder varsayılan olarak Vitest kullanıyor ve Angular 21 ile stable oldu. Oluşan spec dosyası da tanıdık:
it('should render title', async () => {
const fixture = TestBed.createComponent(App);
await fixture.whenStable();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, deneme');
});
describe, it, expect ve TestBed yerinde duruyor. Göze çarpan fark, fixture.detectChanges() yerine await fixture.whenStable() yazılması; zoneless bir uygulamada Angular'ın işini bitirmesini bekliyorsun. Vitest varsayılan olarak testleri tarayıcı açmadan, jsdom ile Node içinde koşturuyor, bu yüzden hızlı.
Karma tamamen kapıdan çıkmadı. Builder ayarına "runner": "karma" ekleyerek ya da yeni projeyi ng new --test-runner=karma ile açarak Karma'da kalabilirsin. Mevcut Jasmine testlerini çevirmek için ng generate refactor-jasmine-vitest komutu var. Bir uyarı: fakeAsync, Jasmine için yamalanmış Zone.js'e dayandığından Vitest'te kullanılamıyor; bu tür testleri ayrıca elden geçirmen gerekecek.
Angular Aria ve Diğer Notlar
Angular 21 ile @angular/aria adında yeni bir paket geldi. İçinde accordion, tabs, menu, listbox, combobox, tree gibi yaygın arayüz kalıpları için erişilebilir directive'ler var. Klavye etkileşimini, ARIA attribute'larını, odak yönetimini ve ekran okuyucu desteğini onlar hallediyor; görünüşe hiç karışmıyorlar, stili tamamen sana bırakıyorlar. Paket Angular 21'de developer preview.
Gözden kaçmaması gereken birkaç küçük değişiklik daha var. HttpClient artık varsayılan olarak root injector'da sağlanıyor; provideHttpClient()'ı yalnızca interceptor eklemek gibi bir özelleştirme yapacaksan yazıyorsun. 21.1 ile ChangeDetectionStrategy.Default deprecated oldu, 21.2 ile de onun yerine geçen ChangeDetectionStrategy.Eager adı geldi. Bu isim değişikliğinin neden yapıldığını bir sonraki sürümde çok net göreceğiz.
Angular 21'e Geçiş: ng update
ng update @angular/core@21 @angular/cli@21
Node.js gereksinimi Angular 20 ile aynı: ^20.19.0, ^22.12.0 ya da ^24.0.0. TypeScript tarafında 5.9'dan eski sürümler artık desteklenmiyor.
Güncelleme sırasında iki önemli migration çalışıyor. Uygulaman Zone.js'e dayanıyorsa provideZoneChangeDetection() ekleniyor. Template'lerinde hâlâ *ngIf ve *ngFor varsa ve control flow migration'ını daha önce çalıştırmadıysan, o da bu güncellemeyle birlikte uygulanıyor. Mevcut testlerin Karma'da kalmaya devam ediyor; Vitest'e geçiş senin kararın.
Küçük Bir Deneme
npx @angular/cli@21 new deneme ile bir proje aç. Önce OrderStatus component'ini düz property ile yaz ve ekranın güncellenmediğini gör. Sonra status'u signal'a çevir ve farkı izle.
Ardından kayıt formunu Signal Forms ile kur. Bugün kuracağın bir Angular 21 projesi 21.1'den yeni olacağı için Field ve [field] yerine FormField ve [formField] kullan. Inputlara yazarken "Model" satırının nasıl değiştiğini izle. Son olarak ng test çalıştır ve testlerin hiçbir tarayıcı penceresi açılmadan bitmesini izle.
Signal Forms'u burada deneysel hâliyle tanıdık. Sırada, bu API'nin resource'larla birlikte stable olduğu, OnPush'un varsayılan change detection stratejisi hâline geldiği ve HttpClient'ın Fetch'e geçtiği Angular 22 var.