iT邦幫忙

2025 iThome 鐵人賽

DAY 17
1
Modern Web

Angular 進階實務 30天系列 第 17

Day 17:狀態管理 - 資料流控制 (Data Flow Control) → 資料怎麼流動?

  • 分享至 

  • xImage
  •  

前言

在上一章,我們探討了資料該存在哪裡,從 localStoragesessionStorage 到 Angular 的 ServiceRoute Reuse。但光是知道資料應該存在哪裡還不夠,另一個同樣重要的問題是:

👉 「資料要怎麼在應用程式中流動?」

在前端應用程式中,狀態並不是靜態的。它會隨著使用者操作、API 回應、事件觸發而不斷改變。如果資料流設計不良,很容易出現以下狀況:

  • 資料不同步:A 元件更新了資料,但 B 元件顯示的仍然是舊的。
  • 狀態來源不明確:不知道某個數值是從哪裡來的,是誰改變的。
  • 維護困難:功能一複雜就變成「牽一髮而動全身」。

因此,資料流控制的核心價值在於:

  1. 明確 → 資料的來源與去向清楚。
  2. 可追蹤 → 狀態變更有跡可循。
  3. 易維護 → 團隊成員能快速理解,降低溝通成本。

常見資料流方式總覽

類別 方式 說明 特點 適用場景
單向資料流 @Input() 父元件透過 @Input 傳資料給子元件 ✔️ 清楚直觀,邊界明確 ❌ 無法直接回傳變更 父頁面傳遞使用者資訊、資料下傳
@Output() + EventEmitter 子元件透過 @Output 對父元件回傳事件 ✔️ 可回傳事件給父元件 ❌ 只能父子層,跨層需層層傳遞 表單提交、按鈕操作
Reactive Forms 程式驅動表單,狀態集中於 FormControl ✔️ 單向資料流,擴充性高 ✔️ 支援驗證器/非同步驗證 ❌ 小型表單較繁瑣 中大型表單(登入、註冊、訂單)
Signals (Angular 16+) 新一代響應式 API,狀態容器自動追蹤依賴 ✔️ 語法簡單、少樣板 ✔️ 適合 UI 狀態 ❌ 生態系新,進階功能有限 UI 切換(深色模式)、表單欄位即時更新、輕量級共享
雙向資料流 NgModel 雙向綁定,UI 與資料自動同步 ✔️ 語法直觀 ❌ 狀態來源不明,難以追蹤 小型表單、輸入框
自定義雙向綁定 利用 @Input + @Output 模擬 [(...)] 語法 ✔️ 重用性高,語法簡潔 ❌ 僅適合簡單狀態 可重用元件(數字選擇器、自訂輸入框)
跨組件通訊 Service (共享狀態) 狀態集中於 Service,注入後跨元件使用 ✔️ 簡單直觀 ✔️ 可集中邏輯 ❌ 過度使用會變全域垃圾桶 使用者登入狀態、購物車、跨頁表單
RxJS Observable 使用 Subject / BehaviorSubject 建立資料流 ✔️ 適合事件驅動、非同步流程 ✔️ 支援 operators ❌ 學習曲線高 WebSocket 即時資料、跨元件通知

各方式解析

建立良好的資料流模式是狀態管理的基礎。本篇將分為三個面向,提供下圖跟文字說明:

  1. **單向資料流 (Unidirectional Data Flow):**可觀察藍色、紫色箭頭
  2. **雙向資料流 (Bidirectional Data Flow):**可觀察紅色箭頭
  3. **跨組件通訊 (Cross-Component Communication):**可觀察下圖綠色箭頭

https://ithelp.ithome.com.tw/upload/images/20250901/20162350knQxIRT7Cd.png


一、單向資料流 (Unidirectional Data Flow)

1. 父到子:@Input()

@Component({
  selector: 'user-profile',
  template: `<p>{{ user.name }}</p>`
})
export class UserProfileComponent {
  @Input() user!: User;
}

  • 優點
    • 清楚、直觀,元件邊界分明。
    • 適合資料下傳(data down)。
  • 缺點
    • 僅能單向傳遞,無法直接回傳變更。
  • 適用場景
    • 父頁面傳遞使用者資訊給子元件顯示。

2. 子到父:@Output() + EventEmitter

@Component({
  selector: 'user-form',
  template: `<button (click)="save()">Save</button>`
})
export class UserFormComponent {
  @Output() saved = new EventEmitter<User>();

  save() {
    this.saved.emit({ name: 'Alice' });
  }
}

  • 優點
    • 子元件能回傳事件給父元件。
    • 搭配 @Input,即可形成簡單的資料交換。
  • 缺點
    • 僅限父子層級,跨層時需要層層傳遞。
  • 適用場景
    • 表單提交 → 父元件接收結果。

3. Reactive Forms (響應式表單)

不同於 Template-driven Forms (使用 NgModel )的雙向綁定,Reactive Forms 採用程式驅動,屬於單向資料流。

import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'user-form',
  template: `
    <form [formGroup]="form">
      <input formControlName="name" />
      <p>{{ form.value.name }}</p>
    </form>
  `
})
export class UserFormComponent {
  form = new FormGroup({
    name: new FormControl('')
  });
}

  • 本質:Reactive Forms 的設計是 單向資料流
    • 狀態的唯一真相 (Single Source of Truth) 是 FormControl
    • FormControl → Template:由程式驅動 UI。
    • Template event → FormControl:UI 事件回饋控制器,再觸發 valueChanges
  • 優點
    • 資料流明確,不像 NgModel 那樣隱含雙向綁定。
    • 更容易擴充,支援驗證器 (Validators)、非同步驗證、動態表單。
    • 適合複雜的表單與大型專案。
  • 缺點
    • 相對 NgModel 不直觀,需要額外學習成本。
    • 對於小型表單可能顯得過於繁重。
  • 適用場景
    • 中大型應用程式中的表單(例如:登入、註冊、設定、訂單輸入)。

4. Signal (Angular 16+)

Signal 是 Angular 16 引入的新響應式 API,比 RxJS 更直觀。Signal 本質上是一個狀態容器,能自動追蹤依賴並在資料變動時觸發更新,但他最主要還是用來服務「資料流動」的部分,所以我放在資料流控制。

const users = signal<User[]>([]);

// 1. 儲存資料(有記憶體位置存放 users 陣列)
const currentUsers = users(); // 讀取儲存的資料

// 2. 管理資料流(響應式特性)
const activeUsers = computed(() => users().filter(u => u.active)); // 自動追蹤依賴
effect(() => console.log('用戶數:', users().length)); // 自動響應變化

// 3. 變更狀態(提供變更方法)
users.set([...newUsers]); // 變更操作
users.update(current => [...current, newUser]); // 變更操作
  • 優點:語法簡單,少樣板程式碼,適合 UI 狀態更新。
  • 缺點:目前仍在發展中,生態系支援度較低。
  • 適用場景:表單欄位即時更新、UI 切換(例如深色模式)。

二、雙向資料流 (Bidirectional Data Flow)

1. NgModel 雙向綁定

<input [(ngModel)]="user.name" />
<p>{{ user.name }}</p>

  • 優點
    • 語法直觀,UI 與資料自動同步。
    • 適合表單輸入、簡單互動。
  • 缺點
    • 容易造成「資料來源不明」的問題。
    • 在大型應用中難以追蹤狀態更新。
  • 適用場景
    • 表單欄位、輸入框。

2. 自定義雙向綁定

@Component({
  selector: 'counter',
  template: `<button (click)="increment()">{{ value }}</button>`
})
export class CounterComponent {
  @Input() value = 0;
  @Output() valueChange = new EventEmitter<number>();

  increment() {
    this.valueChange.emit(this.value + 1);
  }
}

使用時:

<counter [(value)]="count"></counter>

  • 優點
    • 結合 @Input@Output,語法簡潔。
    • 元件可重用性高。
  • 缺點
    • 僅適合簡單狀態。
  • 適用場景
    • 可重用 UI 元件(例如自訂的輸入框、數字選擇器)。

三、跨組件通訊 (Cross-Component Communication)

1. 服務注入 (Shared Service)

@Injectable({ providedIn: 'root' })
export class AuthService {
  user = new BehaviorSubject<User | null>(null);
}

  • 優點
    • 可跨層級共享狀態。
    • 搭配 RxJS 更靈活。
  • 缺點
    • 過度使用會讓 Service 變成「全域變數」。
  • 適用場景
    • 使用者登入狀態、購物車。

2. RxJS Observable 訂閱

this.service.data$.subscribe(value => console.log(value));

  • 優點
    • 適合事件驅動架構。
    • 支援非同步流,搭配 operators 可進行轉換。
  • 缺點
    • 學習曲線高。
  • 適用場景
    • WebSocket 即時資料、跨元件通知。

對比分析

  • 單向資料流 → 結構清楚、好維護,專案不大的時候可讀性好。
  • 雙向資料流 → 適合表單輸入,但避免在複雜應用中濫用。
  • 跨組件通訊 → 適合跨層級,但需要小心避免過度依賴 Service 。

常見陷阱與建議

  1. props drilling(層層傳遞 @Input/@Output)

    → 通常是父子層的時候我才會用@Input/@Output,遇到跨層需求或是多個元件要共用資料的時候,建議改用 Service,像是 getUser() 的情況,常常每個頁面要傳送資料的時候都需要,這時候就適合放在 Service 取用,不適合到處傳來傳去。

  2. 濫用雙向綁定

    → 在大型專案中,可能導致狀態來源不明,建議用單向資料流 + 明確事件回傳。

  3. Service 變全域垃圾桶

    → 建議只存該模組需要的狀態,過大狀態交給其他機制(如 RouteReuseStrategy 或持久化),我以前不會用快照跟持久化的時候,一份百來個欄位的表單,切成好幾個頁面填寫,又要切來切去,利用Service做存取管理真的是十分痛苦。


結語

知道有哪些資料流控制 可以處理之後,以後就能自己寫元件嚕。

本篇介紹了三種主要模式:

  1. 單向資料流
    • @Input/@Output: 父子元件資料交換,結構清楚
    • ReactForm: 程式驅動的單向流,狀態集中在 FormControl,適合複雜表單。
    • Signal: Angular 16 之後的新選項,適合處理 UI 狀態與簡單共享。
  2. 雙向資料流
    • NgModel: 典型的雙向綁定,直觀但是在大專案可能會狀態不明,找不到是誰改了它
    • 自定義雙向綁定: 結合@Input/@Output,適合可重用元件,雖然它是自定義雙向綁定,不過在使用上你也可以使用單向資料流的語法。
  3. 跨組件通訊
    • Share Service / RxJs: 適合跨層級狀態共享。
    • Signals:Angular 16 之後的新選項,適合處理 UI 狀態與簡單共享。

👉 下一篇 Day 18:狀態變更 (State Changes),我們將探討資料如何被修改,包含直接賦值、Immutable 更新、Reducer、Effect,以及狀態變更的通知方式。


上一篇
Day 16:狀態管理 - 資料儲存(Data Storage) → 資料在哪裡?
下一篇
Day 18:狀態管理 - 狀態變更(State Changes) → 資料怎麼被修改?
系列文
Angular 進階實務 30天18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
connieleung
iT邦新手 4 級 ‧ 2025-08-31 11:59:05

我建議使用 signal input 和 output function 而不是 Input/Output 裝飾器。

Zoe Wu iT邦新手 4 級 ‧ 2025-09-01 08:41:58 檢舉

沒錯沒錯,感謝您的補充!

在 Angular 19 提供了穩定版的 Signal Input/Output,也是單向資料流,是現代化的父子通訊方式,與 Signal 生態整合更好。

Signal Input/Output:

是專門用於父子組件通訊的 Signal 版本
替代傳統的 @Input() 和 @Output() 裝飾器

// 替代 @Input()
user = input.required<User>();

// 替代 @Output() 
saved = output<User>();

如果讀者需要更多的介紹,可以參考大大的 Signal API in Angular

我要留言

立即登入留言