Angular 18.2 於 2024 年 8 月 14 日釋出,引入了 linkedSignal()——一個實驗性的新 Signal 原語。它解決了 computed() 和可寫 signal() 之間長期存在的一個痛點:如何建立一個既能從外部源派生初始值,又允許本地修改的 Signal。
問題背景
在 Angular Signals 中,computed() 的值是隻讀的(不能 .set()),而普通 signal() 不能自動追蹤依賴。兩者之間有一個gap:
typescript
// ❌ computed() 是隻讀的
const selectedId = computed(() => props.items()[0]?.id);
// selectedId.set(...) → 報錯!
// ❌ signal() 不能跟隨 props 變化自動更新
const selectedId = signal(props.items()[0]?.id);
// 當 props.items() 變化時,selectedId 不會自動更新
// 以前的變通方案:effect() 手動同步
effect(() => {
selectedId.set(props.items()[0]?.id); // 繁瑣,且 effect 有副作用語義
});
linkedSignal() 解決方案
typescript
import { Component, input, signal, linkedSignal } from "@angular/core";
@Component({
standalone: true,
selector: "app-list-selector",
template: `
<ul>
@for (item of items(); track item.id) {
<li
[class.selected]="selectedId() === item.id"
(click)="selectedId.set(item.id)"
>
{{ item.name }}
</li>
}
</ul>
`,
})
export class ListSelectorComponent {
items = input.required<{ id: string; name: string }[]>();
// linkedSignal:當 items() 變化時,自動重置為第一個元素的 id
// 但使用者點選選擇後,可以本地修改
selectedId = linkedSignal(() => this.items()[0]?.id ?? null);
// 使用者可以修改:selectedId.set('other-id')
// 當 items() 變化時,自動重置為新列表的第一個元素
}
高階用法:帶前值的派生
linkedSignal() 支援訪問前一個值,實現更復雜的邏輯:
typescript
@Component({ standalone: true, ... })
export class PaginatedListComponent {
items = input.required<Item[]>();
pageSize = input(10);
// 當 items 變化時重置到第 1 頁;當 pageSize 變化時,嘗試保持在合理範圍內
currentPage = linkedSignal<number>({
source: () => ({ items: this.items(), pageSize: this.pageSize() }),
computation: (source, previous) => {
const maxPage = Math.ceil(source.items.length / source.pageSize);
if (!previous) return 1; // 初始化
// 保持當前頁,但不超過最大頁數
return Math.min(previous.value, maxPage);
}
});
totalPages = computed(() =>
Math.ceil(this.items().length / this.pageSize())
);
}
linkedSignal vs computed vs signal
signal()
+ 可寫(.set(), .update())
- 不追蹤依賴,不會自動更新
用途:獨立的本地狀態
computed()
+ 自動追蹤依賴,響應式更新
- 只讀,不可手動修改
用途:從其他 Signal 派生的只讀值
linkedSignal() ← 新增
+ 從 Signal 派生初始值(類似 computed)
+ 可寫,允許本地修改(類似 signal)
- 當源 Signal 變化時,本地修改會被重置
用途:有"預設值來自外部,但使用者可覆蓋"語義的狀態
常見應用場景
typescript
// 場景 1:表單欄位的預設值來自 props,使用者可修改
editName = linkedSignal(() => this.user().name);
// 場景 2:多選時,外部列表變化時清空選中
selectedIds = linkedSignal<Set<string>>({
source: this.items,
computation: () => new Set(), // 列表變化時清空
});
// 場景 3:分頁器,資料來源變化時回到第 1 頁
page = linkedSignal<number>({
source: () => this.query(),
computation: (_, prev) => (prev ? 1 : 1),
});
注意:實驗性 API
linkedSignal() 在 Angular 18.2 中仍是實驗性(experimental)API,標記為 @experimental。不排除在未來版本中有 API 調整。預計 Angular 19 會進一步穩定它。
typescript
// 當前匯入路徑中帶有實驗標記
import { linkedSignal } from "@angular/core";
// 使用時 IDE 會提示這是實驗性 API,需自行判斷是否在生產中使用
總結
linkedSignal() 填補了 Angular Signals 原語體系中的一個重要空缺——"可變的派生訊號"。它讓"選中項跟隨列表變化重置,但使用者操作優先"這類常見 UI 模式有了優雅的表達方式。配合 Angular 18.1 已穩定的 Signal API,Angular 的響應式模型正在變得越來越完整。