Angular 19: Varsayılan Standalone, linkedSignal ve resource() ile Veri Yükleme
Angular 19'da standalone: true yazmaya gerek kalmıyor. Birbirine bağlı state için linkedSignal'ı, veri yüklemek için deneysel resource() ve rxResource'u Angular 12 kalıbıyla kıyaslıyoruz.
Sürüm notu: Bu yazı Angular 19 ile gelen yenilikleri anlatır; kodlar Angular 19 içindir.
Angular 19 Masaya Ne Getirdi?
Angular 18'de zoneless change detection'ı deneysel olarak açabildiğimizi, control flow'un stable olduğunu ve template içinde @let ile değişken tanımlayabildiğimizi gördük. 19 Kasım 2024'te çıkan Angular 19 bu birikimin üstüne iki önemli şey koyuyor.
Birincisi küçük görünen ama her dosyaya dokunan bir değişiklik: yıllardır component'lere tek tek yazdığımız bir satır artık gereksiz. İkincisi signal dünyasına iki yeni araç: birbirine bağlı state için linkedSignal, sunucudan veri yüklemek için resource(). Yazının sonunda SSR tarafına kısaca uğrayıp ng update ile geçişe bakacağız.
Angular 12'de Her Component Bir Module'e Aitti
Blok A boyunca bir component'i kullanabilmek için önce bir NgModule'e kaydetmemiz gerekiyordu:
// product-card.component.ts
@Component({
selector: 'app-product-card',
templateUrl: './product-card.component.html'
})
export class ProductCardComponent { }
// app.module.ts
@NgModule({
declarations: [AppComponent, ProductCardComponent],
imports: [BrowserModule],
bootstrap: [AppComponent]
})
export class AppModule { }
Component bir yerde yazılıyor, başka bir yerde tanıtılıyordu. declarations listesine eklemeyi unuttuğunda "is not a known element" hatasıyla karşılaşıyordun. Standalone component'ler bu bağı kopardı, ama Angular 15'ten 18'e kadar her standalone component'e standalone: true yazmak zorundaydık.
Angular 19 ile standalone: true Yazılmıyor
import { Component } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
@Component({
selector: 'app-product-card',
imports: [CurrencyPipe],
template: `
<h3>{{ name }}</h3>
<p>{{ price | currency:'TRY' }}</p>
`
})
export class ProductCardComponent {
name = 'Mekanik Klavye';
price = 1450;
}
Decorator'da standalone diye bir alan yok, yine de bu component standalone. Angular 19'da component, directive ve pipe'lar için varsayılan değer true oldu. Component kendi bağımlılığını imports içinde söylüyor; burada sadece CurrencyPipe'a ihtiyacı var.
Peki hâlâ NgModule kullanan bir projede ne oluyor? Bir module'ün declarations listesindeki component artık açıkça standalone: false demek zorunda. Telefonundaki bir ayarın fabrika değerinin değişmesi gibi düşün: eskiden her seferinde açtığın şey artık açık geliyor, kapalı kalmasını istiyorsan bunu söylemen gerekiyor.
İyi haber şu ki bunu elle yapmıyorsun. ng update sırasında çalışan migration, mevcut standalone: true satırlarını siler ve module'e bağlı component'lere standalone: false ekler. Takımında NgModule'e geri dönüşü tamamen kapatmak istersen tsconfig.json içindeki angularCompilerOptions altına "strictStandalone": true koyabilirsin; o zaman standalone: false yazan component derleme hatası verir.
Birbirine Bağlı State: linkedSignal
Şu senaryoyu düşün: bir kargo seçici component'i dışarıdan seçenek listesi alıyor. Liste değişince seçim ilk seçeneğe dönmeli, ama kullanıcı da istediği seçeneğe tıklayabilmeli. Angular 12'de bunu ngOnChanges ile çözerdik:
export class ShippingPickerComponent implements OnChanges {
@Input() options: string[] = [];
selected = '';
ngOnChanges(changes: SimpleChanges): void {
if (changes.options) {
this.selected = this.options[0];
}
}
choose(option: string): void {
this.selected = option;
}
}
Çalışır, ama "seçim, listeye bağlı" bilgisi bir lifecycle hook'un içine gömülü. Angular 19 ile aynı şey tek satır:
import { Component, input, linkedSignal } from '@angular/core';
@Component({
selector: 'app-shipping-picker',
template: `
@for (option of options(); track option) {
<button (click)="selected.set(option)">{{ option }}</button>
}
<p>Seçili: {{ selected() }}</p>
`
})
export class ShippingPickerComponent {
options = input<string[]>([]);
selected = linkedSignal(() => this.options()[0]);
}
options, daha önce gördüğümüz signal input'lardan biri. selected ise ikisinin arasında duran yeni bir şey. computed gibi kaynağını izler: options değiştiği an yeniden hesaplanıp ilk seçeneğe döner. Ama computed'dan farklı olarak yazılabilir: butona tıklandığında selected.set(option) ile değeri elle değiştirebilirsin.
Navigasyon uygulaması gibi düşün. Önerilen rotadan sapıp başka bir sokağa girebilirsin, uygulama buna izin verir. Ama varış noktasını değiştirdiğin an rota baştan hesaplanır.
Bazen liste değiştiğinde kullanıcının seçimini korumak istersin, tabii seçtiği şey yeni listede hâlâ varsa. Bunun için uzun yazımı kullanıyorsun:
selected = linkedSignal<string[], string>({
source: this.options,
computation: (opts, previous) =>
previous && opts.includes(previous.value) ? previous.value : opts[0]
});
source izlenen signal, computation ise yeni değeri üreten fonksiyon. İkinci parametre olan previous, bir önceki değeri taşıyor; böylece "eski seçim hâlâ geçerli mi?" sorusunu sorabiliyorsun. Angular 19'da linkedSignal developer preview durumunda.
Veri Çekmenin Yeni Yolu: resource()
Angular 12'de bir listeyi sunucudan çekmek aşağı yukarı hep aynı kalıba otururdu:
export class UserListComponent implements OnInit {
users: User[] = [];
loading = false;
error = '';
constructor(private http: HttpClient) { }
ngOnInit(): void {
this.loading = true;
this.http.get<User[]>('/api/users').subscribe({
next: users => { this.users = users; this.loading = false; },
error: () => { this.error = 'Kullanıcılar yüklenemedi'; this.loading = false; }
});
}
}
Üç ayrı property, iki yerde loading = false, bir de sıralama değiştikçe isteği yenilemek istersen araya bir Subject ve switchMap girecek. Angular 19 bu kalıbı tek bir yapıya topluyor:
import { Component, resource, signal } from '@angular/core';
interface User {
id: number;
name: string;
}
@Component({
selector: 'app-user-list',
template: `
<button (click)="sort.set('asc')">A-Z</button>
<button (click)="sort.set('desc')">Z-A</button>
@if (usersResource.isLoading()) {
<p>Yükleniyor...</p>
} @else if (usersResource.error()) {
<p>Kullanıcılar yüklenemedi</p>
} @else {
@for (user of usersResource.value(); track user.id) {
<p>{{ user.name }}</p>
}
}
`
})
export class UserListComponent {
sort = signal<'asc' | 'desc'>('asc');
usersResource = resource({
request: () => ({ sort: this.sort() }),
loader: async ({ request, abortSignal }) => {
const response = await fetch(`/api/users?sort=${request.sort}`, { signal: abortSignal });
return (await response.json()) as User[];
}
});
}
resource() iki fonksiyon alıyor. request, isteğin hangi signal'lara bağlı olduğunu söylüyor; burada sort. loader ise asıl işi yapan, Promise döndüren fonksiyon. sort değiştiğinde Angular loader'ı kendiliğinden yeniden çalıştırıyor. Yarıda kalan eski istek varsa abortSignal sayesinde iptal edilebiliyor.
Dönen nesne bir grup signal taşıyor: value() veriyi, isLoading() yüklenme durumunu, error() hatayı veriyor. Template bunları okuduğu için ekranda ne göstereceğimize karar vermek birkaç @if'ten ibaret. Elle tuttuğumuz loading ve error property'leri tamamen ortadan kalktı.
Bir uyarı: resource() Angular 19'da experimental. API'nin şekli henüz oturmadı; nitekim bir sonraki sürümde bazı parametre adları değişecek. Denemek için harika, ama büyük bir projeye yaymadan önce bunu bilerek karar ver.
HttpClient'tan Vazgeçmeden: rxResource
resource() Promise ile çalışıyor ve RxJS'e bağlı değil. Peki interceptor'ların, hata yönetiminin olduğu HttpClient'ı bırakmak istemiyorsan? Bunun için @angular/core/rxjs-interop paketinden gelen rxResource var:
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { rxResource } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-user-list',
template: `...`
})
export class UserListComponent {
private http = inject(HttpClient);
sort = signal<'asc' | 'desc'>('asc');
usersResource = rxResource({
request: () => ({ sort: this.sort() }),
loader: ({ request }) =>
this.http.get<User[]>('/api/users', { params: { sort: request.sort } })
});
}
Tek fark, loader'ın Promise yerine observable döndürmesi. Template tarafı birebir aynı kalıyor. HttpClient'ın çalışması için uygulamanın app.config.ts dosyasında provideHttpClient() olması gerektiğini unutma. Bir detay daha: rxResource observable'dan yalnızca ilk değeri alıyor, yani sürekli akan bir stream için değil, tek seferlik istekler için tasarlanmış. O da Angular 19'da experimental.
Signal API'leri ve Diğer Durum Değişiklikleri
Angular 19, daha önce gördüğümüz signal tabanlı component API'lerinin çoğunu stable yaptı: input(), output(), model(), viewChild(), viewChildren(), contentChild(), contentChildren() ve takeUntilDestroyed(). Template'teki @let de developer preview'dan çıkıp stable oldu.
effect(), toSignal() ve toObservable() ise bu sürümde hâlâ developer preview. Zoneless da hâlâ experimental; yeni bir projeyi zoneless başlatmak istersen ng new --experimental-zoneless bayrağını kullanabilirsin.
Gözden kaçabilecek küçük bir değişiklik daha var. Uygulama açılırken bir şey yüklemek için kullandığımız APP_INITIALIZER token'ının yerine daha sade bir fonksiyon geldi:
providers: [
provideAppInitializer(() => inject(ConfigService).load())
]
SSR Tarafında Kısa Bir Not: Incremental Hydration
Uygulamayı sunucuda render ediyorsan, Angular 19'daki incremental hydration seni ilgilendirir. Normalde tarayıcı sunucudan gelen HTML'i aldıktan sonra sayfanın tamamını bir anda "canlandırır". Incremental hydration ile bu işi parça parça, ihtiyaç oldukça yaptırabiliyorsun:
// app.config.ts
providers: [provideClientHydration(withIncrementalHydration())]
<!-- template -->
@defer (on timer(15s); hydrate on interaction) {
<app-comments />
}
hydrate on interaction, sunucudan gelen yorum bölümünün kullanıcı ona dokunana kadar ölü bir HTML olarak kalacağını söylüyor. Restoranda menünün masaya baştan gelmesi ama garsonun ancak sen el kaldırınca yanına gelmesi gibi. Bu özellik Angular 19'da developer preview. Aynı sürümde event replay stable oldu ve route bazında render modu seçebilme imkânı geldi.
Angular 19'a Geçiş: ng update
Angular 18'deki bir projeyi şu komutla yükseltiyorsun:
ng update @angular/core@19 @angular/cli@19
CLI birden fazla major sürümü tek seferde atlamana izin vermiyor; 17'deysen önce 18'e, sonra 19'a geçmen gerekiyor. Angular 19 için Node.js tarafında ^18.19.1, ^20.11.1 ya da ^22.0.0 serilerinden biri gerekiyor. TypeScript en az 5.5 olmalı; 19.2 ile 5.8'e kadar olan sürümler destekleniyor.
Güncelleme sırasında standalone migration'ı kendiliğinden çalışıyor ve az önce anlattığımız standalone: true / standalone: false düzenlemesini yapıyor. Komut bittikten sonra git diff ile neyin değiştiğine bir göz at; migration'ın dokunduğu dosyaları görmek, yeni varsayılanı kafana oturtmanın en hızlı yolu.
Küçük Bir Deneme
npx @angular/cli@19 new deneme ile bir proje aç. İçine kargo seçici component'ini yaz, AppComponent içinde bir signal'da iki farklı seçenek listesi tut ve bir butonla listeler arasında geçiş yap. Önce kısa linkedSignal yazımını dene, bir seçeneğe tıkla, sonra listeyi değiştir ve seçimin sıfırlandığını gör. Ardından uzun yazıma geçip seçimin korunduğu durumu yakala.
İkinci adımda bir resource() kur: id adında bir signal tut ve loader içinde https://jsonplaceholder.typicode.com/users/${request.id} adresinden tek bir kullanıcı çek. "Sonraki" butonuna art arda hızlıca bas ve tarayıcının Network sekmesinde yarıda kalan isteklerin iptal edildiğini izle.
Bu yazıdaki request parametresini aklının bir köşesinde tut. Sırada, signal API'lerinin büyük kısmının stable olduğu, resource'ta bu parametrenin adının değiştiği, httpResource'un sahneye çıktığı ve app.component.ts yerine app.ts dosyalarının geldiği Angular 20 var.