Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

Angular 19 – resource API Özelliğini İnceleyelim

Merhaba,

Bu içeriğimizde Angular 19 ile gelen, asenkron veri getirme ve durum yönetimi işlemlerini daha basit ve efektif hale getiren resource API özelliğini inceleyeceğiz.

resource API Nedir?

Resource API’ı anlamak için en doğrusu Angular 19’dan önce bir durumu barındıran aşağıdaki senaryoya karşın gösterdiğimiz tepkiyi ele almak ve oradaki davranışa karşın resource API’ın davranışını mukayesede bulunmaktır. Şöyle ki;

import { Component, effect, inject, signal, WritableSignal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { User } from './models/user';
import { UserService } from './services/user.service';
import { FormsModule } from '@angular/forms'

@Component({
  selector: 'app-root',
  imports: [FormsModule],
  template: `
    <h4>User List</h4>
    <select (change)="userSelected($event)">
      @for (user of userList(); track user) {
        <option [value]="user.id">{{user.name}}</option>
      }
    </select>

    <hr>
    @if (userDetail()) {
      <label>{{userDetail()?.name}}</label>
      <label>{{userDetail()?.email}}</label>
    }
  `
})
export class AppComponent {
  userId: WritableSignal<number> = signal<number>(1);
  userList: WritableSignal<User[]> = signal<User[]>([] as User[]);
  userDetail: WritableSignal<User | undefined> = signal<User | undefined>(undefined);

  userService: UserService = inject(UserService);

  async ngOnInit() {
    const allUsers: User[] = await this.userService.getUsers();
    this.userList.set(allUsers);
    const userDetail = await this.userService.getUserById(this.userId());
    this.userDetail.set(userDetail);
  }

  userSelected(event: any) {
    console.log(event.target.value);
    this.userId.set(event.target.value);
  }
}

Angular 19 - resource API Özelliğini İnceleyelimYukarıdaki çalışmayı ele alırsak eğer kullanıcı listesinden seçilen kullanıcının detay bilgileri yapılan yeni bir istek neticesinde elde edilmekte ve aşağıda gösterilmek istenmektedir. Tabi bunun için userSelected metodunda gerekli işlemler yapılmakta ancak görüldüğü üzere UI’da bu işlemlere karşın bir değişim durumu söz konusu olmamaktadır. Yani anlayacağınız, her ne kadar Angular’ın son dönemlerdeki gözdesi olan signal‘ları kullanıyor olsak da bu durumda pek de işimize yaramamaktadır. Neden? diye sorarsanız, cevap olarak signal’ların başlangıç değerlerine göre işlem yapmalarını verebiliriz. Yani signal’lar ilk tanımlandıkları değer neyse ona göre işlevsellik göstermekte, sonradan değeri değişse bile bunla ilgili bir tutum sergilememektedirler. Ee bu açıdan bakıldığında da signal’lar ile yaptığımız bu tarz çalışmalar beklenildiği gibi tam anlamı ile reaktif bir davranış sergilememiş olmaktadırlar.

Resource API özelliğinden önce bu işlemi beklenildiği hale getirebilmek için effect yapısından istifade etmemiz gerekmekteydi. Ee hocam computed fonksiyonu da var! dediğinizi duyar gibiyim… Evet, computed fonksiyonu da var, var olmasına ama computed fonksiyonu senkron yapılar için tasarlandığından genellikle asenkron süreçlerde istemsiz neticelerle süreci sonlandırabiliyor. Şöyle ki, ...getUserById(1) isteğinin yapıldığını düşünelim ve sonucunda geç döneceğini varsayalım. Bu istek neticelenmeden üzerine ...getUserById(2) isteğinin yapıldığını düşünelim. İşte bu durumda computed fonksiyonu bir önceki isteği iptal etmeyecek ve önceki isteğin sonucu sonrakinin ardından gelirse ekrana onun yani öncekinin neticesini gösterecektir. Ee haliyle bu durumda ne kullanıcı yaptığından ne de uygulama hangi state’de olduğundan bişey anlayacaktır… İşte buna benzer durumlar computed fonksiyonunda söz konusu olabileceğinden dolayı bizler effect yapısını kullanarak bu tarz durumlara karşın çözüm getirmeyi daha uygun buluyorduk.

Şimdi gelin yukarıdaki çalışmayı effect ile modifiye edelim;

export class AppComponent {
  userId: WritableSignal<number> = signal<number>(1);
  userList: WritableSignal<User[]> = signal<User[]>([] as User[]);
  userDetail: WritableSignal<User | undefined> = signal<User | undefined>(undefined);

  userService: UserService = inject(UserService);

  async ngOnInit() {
    const allUsers: User[] = await this.userService.getUsers();
    this.userList.set(allUsers);
  }

  userSelected(event: any) {
    console.log(event.target.value);
    this.userId.set(event.target.value);
  }

  //#region effect İle Çözüm
  _userDetail = effect(async () => {
    const userDetail = await this.userService.getUserById(this.userId());
    this.userDetail.set(userDetail);
  });
  //#endregion
}

Angular 19 - resource API Özelliğini İnceleyelimEvet, görüldüğü üzere mevzu bahis problemin effect fonksiyonu sayesinde çözüldüğü düşünülebilir, lakin burada bir handikap söz konusu. Bu handikap, effect içerisinde kullanılan userId signal değeri farklı bir yerden değiştirildiğinde aynı effect‘in tekrar tetiklenmesi durumudur. Bu minvalde yaptığımız çalışmaya göz atarsak eğer userId değeri başka gerekçelerle değişebilme ihtimaline sahiptir.

İşte bu noktada en ideal çözümü Angular 19 ile birlikte gelen resource API sunabilmektedir. resource API sayesinde bizler signal’larla yapılan asenkron süreçleri daha etkin bir şekilde kullanabilmekte ve uygulamayı yukarıda simüle etmeye çalıştığımız durumlarda olduğu gibi reaktif hale getirebilmekteyiz. Bunun için aşağıdaki çalışmaya göz atarsak eğer;

import { Component, effect, inject, resource, ResourceRef, signal, WritableSignal } from '@angular/core';
import { User } from './models/user';
import { UserService } from './services/user.service';
import { FormsModule } from '@angular/forms'

@Component({
  selector: 'app-root',
  imports: [FormsModule],
  template: `
    <h4>User List</h4>
    <select (change)="userSelected($event)">
      @for (user of userList(); track user) {
        <option [value]="user.id">{{user.name}}</option>
      }
    </select>

    <hr>
    @if (userDetail.value()) {
      <label>{{userDetail.value()?.name}}</label>
      <label>{{userDetail.value()?.email}}</label>
    }
  `
})
export class AppComponent {
  userId: WritableSignal<number> = signal<number>(1);
  userList: WritableSignal<User[]> = signal<User[]>([] as User[]);

  //#region resource API İle Çözüm
  userDetail = resource<User, number>({
    request: () => this.userId(),
    loader: async ({ request: userId }) => {
      return await this.userService.getUserById(userId);
    }
  })
  //#endregion

  async ngOnInit() {
    const allUsers: User[] = await this.userService.getUsers();
    this.userList.set(allUsers);
  }

  userService: UserService = inject(UserService);

  userSelected(event: any) {
    console.log(event.target.value);
    this.userId.set(event.target.value);
  }
}

Evet… Yukarıdaki çalışmaya göz atarsak eğer resource API ile userId‘ye yeni bir değer atandığı her an ilgili kullanıcıya dair detay isteği yeniden gönderilecek ve elde edilecektir. İşte bu kadar basit. Tabi buradaki işleyişi daha net anlayabilmek için bu resource API’ın anatomik incelemesini daha detaylı bir şekilde yapmamız gerekecektir. Bunun için ilgili fonksiyonun yapısını aşağıya sade bir şekilde alalım;

export declare function resource<T, R>(options: ResourceOptions<T, R>): ResourceRef<T | undefined>;

Dikkat ederseniz, resource fonksiyonu esasında ResourceOptions parametresi alan ve geriye ResourceRef türünden bir nesne döndüren fonksiyondur. İlk olarak ResourceOptions parametresinin yapısını incelersek;

export declare interface ResourceOptions<T, R> {
    request?: () => R;
    loader: ResourceLoader<T, R>;
    .
    .
}

Görüldüğü üzere bu türün request ve loader olmak üzere iki ana özelliği mevcuttur. request özelliği, request değerini temsil eder ve reaktif bir şekilde davranış sergiler. Bu özelliğe verilen değer değiştiği taktirde bu yeni bir request değeri olarak anlaşılacaktır. Evet, bu davranış computed fonksiyonunun davranışını anımsatmaktadır.

loader özelliğine gelirsek eğer her yeni request değeri üretildiğinde/geldiğinde bu değer bu fonksiyona iletilmekte ve yeni değer ile birlikte bu fonksiyon tetiklenmektedir. Dikkat ederseniz bu fonksiyon ResourceLoader türünde tanımlanmaktadır. Yani bir başka deyişle bu fonksiyonu tanımlarken esasında ResourceLoader bildiriminde bulunulmaktadır. Hemen bu türün detayına göz atarsak eğer;

export declare type ResourceLoader<T, R> = (param: ResourceLoaderParams<R>) => PromiseLike<T>;

bunun geriye PromiseLike türünden bir değer döndüren ve ResourceLoaderParams türünden de parametre alan bir fonksiyon olduğunu görüyoruz. Bu parametrenin içeriğine bakarsak eğer;

export declare interface ResourceLoaderParams<R> {
    request: Exclude<NoInfer<R>, undefined>;
    abortSignal: AbortSignal;
    .
}

şeklinde olduğunu göreceğiz. Burada request özelliği, adı üzerinde request yapılacak değeri ifade etmektedir. abortSignal özelliğini anlayabilmek için ise resource API’nin genel davranışına hakim olmak gerekmektedir. resource API, request değeri değiştiği an önceki değerin isteğini iptal eden bir davranışa sahiptir. İşte bizler bu davranış sürecinde abortSignal ile iptal edilen isteklere dair bir dönüş yapabilmekteyiz.

Tabi ayrıca resource API’ı iradeli bir şekilde reload fonksiyonuyla aşağıdaki gibi tetiklenebileceğini de bilmekte fayda vardır;

.
.
  reload() {
    this.userDetail.reload();
  }

Kimi zaman aynı request değerinde tekrar bir yükleme yapılması gerektiği durumlar mevzu bahis olabilmekte ve o taktirde bu fonksiyon imdadımıza yetişmektedir.

Son olarak resource fonksiyonunun geri dönüş değeri olan ResourceRef türünün detaylarını inceleyelim. Yani resource status’ü.

Resource status sayesinde, resource API’ın loader fonksiyonunun asenkron süreçteki durumunu aşağıdaki gibi çeşitli signal’larla takip edebilmekteyiz;

Özellik Açıklama
value resource fonksiyonundan dönen değeri ifade eder. Eğer asenkron süreç neticesinde bir değer döndürülmediyse undefined olacaktır.
hasValue Henüz bir değerin dönüp döndürülmediğini ifade eder.
error resource fonksiyonunun işlev ve yaptığı request sürecinde varsa bir hata getirecek, yoksa undefined dönecektir.
isLoading resource fonksiyonunun çalışıp, çalışmadığını belirtir.
status resource’un state’ini belirtmektedir.

Haliyle bu özellikleri kullanarak resource API üzerinden yürütülen request’lerin sürecini görsel olarak da html kısmına aşağıdaki gibi yansıtabiliriz;

    @if(!userDetail.hasValue() || userDetail.isLoading()){
      <div>
        Yükleniyor...
      </div>
    }

Angular 19 - resource API Özelliğini İnceleyelimYapısal olarak resource API davranışının switchMap semantiğine sahip olduğunu bilmenizde fayda görmekteyim. switchMap bir RxJS operatörüdür ve özellikle nested observable’ları yönetmek için kullanılmaktadır. Yani önceki bir observable’daki süreci iptal edip yeni bir observable başlatmak istiyorsak ya da başka bir deyişle kullanıcı etkileşimlerindeki sabırsızlıklardan kaynaklı önceki işlemlerin iptal edilip en son yapılan işlemin geçerli kılınması gerekiyorsa switchMap operatöründen istifade etmekteyiz. Evet, resource API’de benzer mantıkta bir davranış benimsemektedir ve önceki istekleri her ne kadar iptal etmese de en azından sonuçlarını yok saymakta ve kullanıcının telaşlı bir şekilde ekranda yaptığı eylemlerden kaynaklı sistemin şirazesiyle oynamamaktadır 🙂

Nihai olarak;
Angular 19 ile gelen resource API özelliği sayesinde, API’lere gönderilecek request süreçlerini oldukça etkili bir şekilde reaktif hale getirebildiğimizi ve böylece uygun senaryolarda daha efektif davranışlar sergileyerek olması gereken çözümü oldukça pratik bir şekilde uygulayabildiğimizi görmüş bulunuyoruz. Artık gerisi projelerimizde bu özellikten bol bol istifade ederek maksimum verim almaya kalmıştır 🙂

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

1 Cevap

  1. Farman Guliyev dedi ki:

    Guzelmis

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir