iT邦幫忙

2024 iThome 鐵人賽

0
JavaScript

Signal API in Angular系列 第 31

Day 31 - 請不要使用 effect

  • 分享至 

  • xImage
  •  

我觀看了 YouTube 視頻,其中 Angular 團隊負責人 Alex Rickabaugh 不鼓勵使用 effect。 然後,他示範了一種用 computed signal 取代 effect 的方法,這種方法並不直觀,並且需要開發人員進行思維轉變,在 computed signal 中包含 WritableSignal

今天,我想用 signalscomputed signals 來取代 explicitEffect

之前的 explicitEffect 程式碼

searchId = signal(initialId); 
id = signal(initialId);
 person = signal<undefined | Person>(undefined);
 films = signal<string[]>([]);
 rgb = signal('brown');
 fontSize = computed(() => this.id() % 2 === 0 ? '1.25rem' : '1.75rem');

該元件有一些 signals 來儲存 searchIdidpersonfilmsrgb 程式碼。 fontSize computed signal 根據 id 得出字體大小。

 #logIDsEffect = explicitEffect([this.searchId],
   ([searchId]) => console.log('id ->', this.id(), 'searchID ->', searchId), { defer: true });

 #rgbEffect = explicitEffect([this.rgb], ([rgb]) => console.log('rgb ->', rgb), { defer: true });

 constructor() {
   explicitEffect([this.id], ([id], onCleanUp) => {
     const sub = getPersonMovies(id, this.injector)
       .subscribe((result) => {
         if (result) {
           const [person, ...rest] = result;
           this.person.set(person);
           this.films.set(rest);
         } else {
           this.person.set(undefined);
           this.films.set([]);
         }

         this.rgb.set(generateRGBCode());
         this.rgb.set(generateRGBCode());
         this.rgb.set(generateRGBCode());
       });

     this.renderer.setProperty(this.hostElement, 'style', `--main-font-size: ${this.fontSize()}`);
     if (id !== this.searchId()) {
       this.searchId.set(id);
     }

     onCleanUp(() => sub.unsubscribe());
   });
 }

此元件具有三種 effect,可在控制台中記錄 signals 或更新 signals 。這些 signals 需要用 computed state 取代。

取代 fontSize 計算訊號 (computed signal)

程式碼審查後,流程是保留 id signal 並消除其餘 signals。 第一步是新增 computed state 並刪除 fontSize computed signal。

state = computed(() => {
   return {
     fontSize: this.id() % 2 === 0 ? '1.25rem' : '1.75rem',
   };
 });

id signal 更新時,state computed signal 會為 fontSize 屬性匯出新的字體大小。

host: {
   '[style.--main-font-size]': 'state().fontSize',
 },

使用 host 屬性而不是 Renderer2ElementRef 來更新 CSS 變數。

取代 rgb signal

state = computed(() => {
   return {
     fontSize: this.id() % 2 === 0 ? '1.25rem' : '1.75rem',
     rgb: generateRGBCode(),
   };
 });

id signal 改變時, state computed signal 會為 rgb 屬性匯出新的 RGB 值。同樣,host 屬性也會更新 CSS 變數,並刪除 #rgbEffect effect,以便它不會記錄 rgb 變更。

host: {
   '[style.--main-color]': 'state().rgb',
},

替換 searchId signal

searchId signal 比其他 signals 需要更多的工作。 當 id signal 更新時,它也具有相同的值。 當 seachId signal 發生變化時,id signal 也接收到最新的值。

state = computed(() => {
   const result = this.#personMovies();
   return {
     fontSize: this.id() % 2 === 0 ? '1.25rem' : '1.75rem',
     rgb: generateRGBCode(),
     searchId: signal(this.id()),
   };
 });

state computed signal 中,searchId 屬性是一個初始值為 this.id() 的 signal。 當 id signal 隨後發生變化時,state computed signal 會同步 searchId 屬性的值。

syncId(id: number) {
   if (id >= this.min && id <= this.max) {
     this.state().searchId.set(id);
     this.id.set(id);
   }
}

當使用者在文字欄位中輸入新的 id 時,syncId 方法會設定 searchId 屬性和 id signal。

<input type="number" [ngModel]="state().searchId()" (ngModelChange)="syncId($event)" />

輸入欄位不能使用雙向資料綁定將 searchId signal 綁定到 ngModel directive。 ngModelChange event emitter 呼叫 syncId 方法來更新 signal。

在 constructor 中,刪除RxJS程式碼,因為它沒有被使用。

toObservable(this.searchId).pipe(
     debounceTime(300),
     distinctUntilChanged(),
     filter((value) => value >= this.min && value <= this.max),
     map((value) => Math.floor(value)),
     takeUntilDestroyed(),
   ).subscribe((value) => this.id.set(value));

syncId 方法相比,我更喜歡上面的RxJS程式碼;我寧願使用 effect 來同步 idsearchId signals 的值。

使用 toSignal 和 toObservable 發出 HTTP 請求

function getPersonMovies(http: HttpClient) {
 return function(source: Observable<Person>) {
   return source.pipe(
     mergeMap((person) => {
       const urls = person?.films ?? [];
       const filmTitles$ = urls.map((url) => http.get<{ title: string }>(url).pipe(
         map(({ title }) => title),
         catchError((err) => {
           console.error(err);
           return of('');
         })
       ));

       return forkJoin([Promise.resolve(person), ...filmTitles$]);
     }),
     catchError((err) => {
       console.error(err);
       return of(undefined);
     }));
       }
}

這是一個自訂 RxJS 運算子,用於檢索星際大戰角色的詳細資訊和影片。

#personMovies = toSignal(toObservable(this.id)
   .pipe(
     switchMap((id) => this.http.get<Person>(`${URL}/${id}`)
       .pipe(getPersonMovies(this.http))
     ),
 ), { initialValue: undefined });

#personMovies 使用 toSignal 和 toObservable 函數建立星際大戰詳細資訊的 signal。 我覺得toSignal(toObservable(this.id)) 很長,對於初學者來說不容易理解。

state = computed(() => {
   const result = this.#personMovies();
   return {
     fontSize: this.id() % 2 === 0 ? '1.25rem' : '1.75rem',
     rgb: generateRGBCode(),
     person: signal(result && result.length > 0 ? result[0] : undefined),
     films: signal(result && result.length > 1 ? result.slice(1): []),
     searchId: signal(this.id()),
   };
});

如果 HTTP 請求成功,則定義 result 陣列。 person 屬性是一個 signal,其值是 result 的第一個元素。 films 屬性是一個 signal,其值是 result 剩餘的元素。

<div class="border">
     @if(state().person(); as person) {
       <p>Id: {{ id() }} </p>
       <p>Name: {{ person.name }}</p>
       <p>Height: {{ person.height }}</p>
       <p>Mass: {{ person.mass }}</p>
       <p>Hair Color: {{ person.hair_color }}</p>
       <p>Skin Color: {{ person.skin_color }}</p>
       <p>Eye Color: {{ person.eye_color }}</p>
       <p>Gender: {{ person.gender }}</p>
     } @else {
       <p>No info</p>
     }

     <p style="text-decoration: underline">Movies</p>
     @for(film of state().films(); track film) {
       <ul style="padding-left: 1rem;">
         <li>{{ film }}</li>
       </ul>
     } @empty {
       <p>No movie</p>
     }
 </div>

HTML 範本根據 state computed signal 顯示人物和影片。

結論:

  • Angular 團隊表示不要濫用 effect
  • 我們可以建立 signas-in-computed,並在它們依賴的 signal 發生變化時更新屬性。
  • 使用 toSignaltoObservable 發出 HTTP 請求。 toSignal(toObservable(this.id)) 又長又難讀,我們可以查看 ngxtension 庫中的 toObservableSignal 函數。

鐵人賽的第 31 天到此結束。

參考:

Techstack Nation Don't use effect: https://www.youtube.com/watch?v=aKxcIQMWSNU&feature=youtu.be
Stackblitz Demo: https://stackblitz.com/edit/stackblitz-starters-cejcoj?file=src%2Fstar-war%2Fstar-war.service.ts


上一篇
Day 30 - Angular 和 Signal 的未來
下一篇
Day 32 - 在 Angular 中從後端檢索資料的不同方法
系列文
Signal API in Angular36
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言