在上一章,我們探討了資料該存在哪裡,從 localStorage
、sessionStorage
到 Angular 的 Service
、Route Reuse
。但光是知道資料應該存在哪裡還不夠,另一個同樣重要的問題是:
👉 「資料要怎麼在應用程式中流動?」
在前端應用程式中,狀態並不是靜態的。它會隨著使用者操作、API 回應、事件觸發而不斷改變。如果資料流設計不良,很容易出現以下狀況:
因此,資料流控制的核心價值在於:
類別 | 方式 | 說明 | 特點 | 適用場景 |
---|---|---|---|---|
單向資料流 | @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 即時資料、跨元件通知 |
建立良好的資料流模式是狀態管理的基礎。本篇將分為三個面向,提供下圖跟文字說明:
@Input()
@Component({
selector: 'user-profile',
template: `<p>{{ user.name }}</p>`
})
export class UserProfileComponent {
@Input() user!: User;
}
@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
,即可形成簡單的資料交換。不同於 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('')
});
}
FormControl
。FormControl → Template
:由程式驅動 UI。Template event → FormControl
:UI 事件回饋控制器,再觸發 valueChanges
。NgModel
那樣隱含雙向綁定。NgModel
不直觀,需要額外學習成本。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]); // 變更操作
NgModel
雙向綁定<input [(ngModel)]="user.name" />
<p>{{ user.name }}</p>
@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
,語法簡潔。@Injectable({ providedIn: 'root' })
export class AuthService {
user = new BehaviorSubject<User | null>(null);
}
this.service.data$.subscribe(value => console.log(value));
props drilling(層層傳遞 @Input/@Output)
→ 通常是父子層的時候我才會用@Input/@Output,遇到跨層需求或是多個元件要共用資料的時候,建議改用 Service,像是 getUser() 的情況,常常每個頁面要傳送資料的時候都需要,這時候就適合放在 Service 取用,不適合到處傳來傳去。
濫用雙向綁定
→ 在大型專案中,可能導致狀態來源不明,建議用單向資料流 + 明確事件回傳。
Service 變全域垃圾桶
→ 建議只存該模組需要的狀態,過大狀態交給其他機制(如 RouteReuseStrategy 或持久化),我以前不會用快照跟持久化的時候,一份百來個欄位的表單,切成好幾個頁面填寫,又要切來切去,利用Service做存取管理真的是十分痛苦。
知道有哪些資料流控制 可以處理之後,以後就能自己寫元件嚕。
本篇介紹了三種主要模式:
@Input
/@Output
: 父子元件資料交換,結構清楚ReactForm
: 程式驅動的單向流,狀態集中在 FormControl
,適合複雜表單。Signal
: Angular 16 之後的新選項,適合處理 UI 狀態與簡單共享。NgModel
: 典型的雙向綁定,直觀但是在大專案可能會狀態不明,找不到是誰改了它@Input
/@Output
,適合可重用元件,雖然它是自定義雙向綁定,不過在使用上你也可以使用單向資料流的語法。👉 下一篇 Day 18:狀態變更 (State Changes),我們將探討資料如何被修改,包含直接賦值、Immutable 更新、Reducer、Effect,以及狀態變更的通知方式。
我建議使用 signal input 和 output function 而不是 Input/Output 裝飾器。
沒錯沒錯,感謝您的補充!
在 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