昨天我們透過 httpResource() 搭配 chain() 處理兩個非同步資料之間的依賴。下游 Request 需要使用上游資料時,會等上游完成後再繼續;當 Request 的來源改變,Resource 的狀態也會跟著更新。
Signal 也會遇到類似情況。有些狀態會依賴其他 Signal,但本身又需要被修改,甚至在來源改變後參考前一次的值。這篇就來看看 linkedSignal() 可以怎麼處理這類狀態。
有些狀態會跟著其他 Signal 改變,但同時也需要讓使用者自行修改。
例如畫面上有公告分類與公告清單。切換分類後,清單會重新篩選,並預設選擇該分類的第一筆公告;使用者也可以再選擇其他公告。
假設目前有以下資料:
先使用 computed() 實作:
import { Component, computed, signal } from '@angular/core';
interface Message {
id: number;
category: string;
title: string;
}
const MESSAGES: Message[] = [
{ id: 1, category: '系統', title: '系統維護通知' },
{ id: 2, category: '活動', title: '週年慶活動' },
{ id: 3, category: '系統', title: '版本更新說明' },
{ id: 4, category: '活動', title: '會員日優惠' },
];
@Component({
selector: 'app-root',
template: `
<button (click)="category.set('全部')">全部</button>
<button (click)="category.set('系統')">系統</button>
<button (click)="category.set('活動')">活動</button>
<ul>
@for (message of messages(); track message.id) {
<li [class.active]="message.id === selectedId()">
{{ message.title }}
</li>
}
</ul>
`,
styles: `
li {
padding: 8px 12px;
margin-bottom: 4px;
border-radius: 6px;
}
li.active {
background: #e0e7ff;
font-weight: 600;
}
`,
})
export class AppComponent {
category = signal('全部');
messages = computed(() =>
this.category() === '全部'
? MESSAGES
: MESSAGES.filter(
(message) => message.category === this.category()
)
);
selectedId = computed(
() => this.messages()[0].id
);
}
messages 會根據 category 篩選,而 selectedId 會取得目前清單的第一筆。
如果還需要讓使用者自行選擇公告,selectedId 就會有兩種更新來源:

分類改變時,可以透過 computed() 重新計算 selectedId;但使用者選擇其他公告時,就需要直接修改它:
this.selectedId.set(3);
computed() 產生的是唯讀 Signal,不能使用 set()。
這裡的 selectedId 除了會跟著 messages 改變,也需要保存使用者目前的選擇。這種有依賴關係,又需要自行修改的狀態,就可以使用 linkedSignal()。
把 selectedId 改成 linkedSignal(),並在使用者選擇公告時透過 set() 更新:
import { Component, computed, linkedSignal, signal } from '@angular/core';
interface Message {
id: number;
category: string;
title: string;
}
const MESSAGES: Message[] = [
{ id: 1, category: '系統', title: '系統維護通知' },
{ id: 2, category: '活動', title: '週年慶活動' },
{ id: 3, category: '系統', title: '版本更新說明' },
{ id: 4, category: '活動', title: '會員日優惠' },
];
@Component({
selector: 'app-root',
template: `
<button (click)="category.set('全部')">全部</button>
<button (click)="category.set('系統')">系統</button>
<button (click)="category.set('活動')">活動</button>
<ul>
@for (message of messages(); track message.id) {
<li
[class.active]="message.id === selectedId()"
(click)="selectedId.set(message.id)"
>
{{ message.title }}
</li>
}
</ul>
`,
styles: `
li {
padding: 8px 12px;
margin-bottom: 4px;
border-radius: 6px;
cursor: pointer;
}
li.active {
background: #e0e7ff;
font-weight: 600;
}
`,
})
export class AppComponent {
category = signal('全部');
messages = computed(() =>
this.category() === '全部'
? MESSAGES
: MESSAGES.filter(
(message) => message.category === this.category()
)
);
selectedId = linkedSignal(
() => this.messages()[0].id
);
}
切換分類時,selectedId 會自動更新為新清單的第一筆;使用者也可以手動選擇其他公告。
computed() 適合唯讀的衍生狀態,而 linkedSignal() 適合有依賴關係、同時又需要修改的狀態。

linkedSignal() 也可以用來處理 Resource 的狀態。
有些情境在查詢條件改變後,不希望畫面立刻清空,而是先保留上一筆結果,等新的 Request 完成後再替換。
一般的 httpResource() 在 Request 來源改變後會重新發出請求並進入 loading,這時原本的 value() 會變成 undefined。
userId = signal(1);
userResource = httpResource<User>(
() => `/api/users/${this.userId()}`
);
當 userId 改變時:
this.userId.set(2);
如果希望新的 Request 執行期間仍保留上一筆資料,可以搭配 Resource 的 snapshot 與 linkedSignal()。
snapshot:取得目前的 Resource 狀態previous.value:取得 linkedSignal() 前一次保存的 Snapshot把 snapshot 作為 linkedSignal() 的來源後,就能在 Resource 進入 loading 時沿用上一筆 value,等新的 Response 回來再替換。

httpResource() 本身就提供 snapshot,因此可以直接把它作為 linkedSignal() 的來源。
先建立一個 withPreviousValue():
import {
linkedSignal,
resourceFromSnapshots,
Resource,
ResourceSnapshot,
} from '@angular/core';
function withPreviousValue<T>(
input: Resource<T>
): Resource<T> {
const snapshot = linkedSignal<
ResourceSnapshot<T>,
ResourceSnapshot<T>
>({
source: input.snapshot,
computation: (current, previous) => {
if (
current.status === 'loading' &&
previous &&
previous.value.status !== 'error'
) {
return {
status: 'loading' as const,
value: previous.value.value,
};
}
return current;
},
});
return resourceFromSnapshots(snapshot);
}
這裡把原本 Resource 的 snapshot 當成來源:
source: input.snapshot
當新的 Request 開始後,current 會進入 loading,而 previous.value 保留前一次的 Snapshot。
因此可以在 loading 期間沿用上一筆資料:
return {
status: 'loading' as const,
value: previous.value.value,
};
此時 Resource 仍然是 loading,但 value 會保留上一筆資料:
status = loading
value = User 1
新的 Request 完成後,current 會帶著新的資料進來,直接回傳即可:
return current;
linkedSignal() 這裡產生的是 Signal<ResourceSnapshot<T>>。
最後再透過 resourceFromSnapshots() 根據這個 Snapshot Signal 建立 Resource<T>,讓外部繼續使用 value()、status()、isLoading() 等 Resource API:
return resourceFromSnapshots(snapshot);
使用時包住原本的 httpResource():
userId = signal(1);
userResource = withPreviousValue(
httpResource<User>(
() => `/api/users/${this.userId()}`
)
);
這時新的 Request 還在執行時,Resource 可能同時處於:
isLoading() = true
hasValue() = true
也就是狀態已經進入 loading,但 value() 仍然保留上一筆資料。
如果希望 Loading 期間繼續顯示上一筆內容,可以直接使用:
@if (userResource.hasValue()) {
<p>{{ userResource.value().name }}</p>
}
等新的結果回來後,value() 再更新成新的資料。

保留上一筆資料後,還可以再調整 Loading 顯示的時機。
有些 Request 很快就會完成,如果 isLoading() 一變成 true 就立刻顯示 Skeleton,畫面容易出現短暫閃爍。
這部分不需要改變 httpResource(),可以另外用 RxJS 延遲 Loading 狀態。
先把 isLoading() 轉成 Observable,在進入 Loading 後等待 300ms,再轉回 Signal:
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { map, of, switchMap, timer } from 'rxjs';
showLoading = toSignal(
toObservable(this.userResource.isLoading).pipe(
switchMap((isLoading) =>
isLoading
? timer(300).pipe(
map(() => true)
)
: of(false)
)
),
{
initialValue: false,
}
);
如果 Request 在 300ms 內完成,isLoading 會先變回 false,switchMap() 會取消前面的 timer(),Skeleton 就不會出現。
這裡因為 withPreviousValue() 會讓 hasValue() 在 Loading 期間維持 true,Template 需要讓 showLoading() 優先:
@if (showLoading()) {
<app-skeleton />
} @else if (userResource.hasValue()) {
<p>{{ userResource.value().name }}</p>
}
前 300ms 會繼續顯示上一筆資料;如果 Request 超過 300ms 還沒完成,才改成 Skeleton。
Request 開始
↓
0 ~ 300ms
保留上一筆資料
↓
超過 300ms
顯示 Skeleton
↓
Request 完成
顯示新資料
兩個需求分別處理不同的事情:
snapshot + linkedSignal
→ 新 Request 完成前保留上一筆資料
RxJS
→ 超過 300ms 才顯示 Skeleton
如果新的 Request 在 300ms 內完成,就直接從舊資料換成新資料,不需要出現 Skeleton。

linkedSignal() 適合處理會受到其他 Signal 影響,但本身仍需要修改的狀態。
和 computed() 相比,可以從狀態是否需要直接修改來區分:
computed()
linkedSignal()
signal()
linkedSignal() 也可以取代部分單純為了同步狀態而使用 effect() 的寫法,把依賴關係直接放在狀態宣告中。
到了 Resource,也可以利用同樣的概念。透過 snapshot 與 previous.value,可以在新的 Request 完成前保留上一筆資料;Loading 的顯示時機則另外交給 RxJS 處理。
不過保留上一筆資料並不適合所有情境。如果舊資料容易被誤認為目前資料,例如切換帳號、病人或其他敏感上下文,就不適合直接沿用上一筆內容,或需要搭配明確的 Loading 狀態。