SWR isLoading 早退會擋住整棵子樹的 API
核心觀念
useSWR 的 isLoading 在第一次 render 就同步回傳 true(只要 key 有效、cache 是空的、準備要 fetch)。
不是等 effect 才變 true。所以「一開始 condition 是 false」是錯的直覺。
isLoading= 有進行中的請求且尚未有資料(cache 空) → 第一次 render 就是true
反模式:用 loading 早退包住 children
const { isLoadingAllProjects } = useProjects({ loadAll: true });
// ❌ 第一次 render isLoadingAllProjects 就是 true → 走這條 early return
if (isLoadingAllProjects) {
return <Skeleton />;
}
return (
<Flex>
<Sidebar items={sidebarItems} />
<Box>{children}</Box> {/* children 被擋在 early return 之後,render #1 根本沒 mount */}
</Flex>
);沒被 render 的元件,裡面的 useSWR / useEffect 都不會執行。
所以 children(真正的頁面)自己的 API 要等:
render #1: hook 送 sidebar 的 GET /projects; isLoading=true → Skeleton → children 沒 mount → 頁面 API 沒打
...等 GET /projects 回來...
render #2: isLoading=false → children 第一次 mount → 頁面 API 這時才起跑
sidebar 有多慢,頁面 API 就被往後推多久(延遲放大器)。
修法:並行,不要 gate
把 loading 狀態往下傳給該負責的子元件自己顯示,別擋住兄弟節點。
// ✅ 一律 render;loading 交給 Sidebar 內部畫 skeleton
return (
<Flex>
<Sidebar items={sidebarItems} isLoading={isLoadingAllProjects} />
<Box>{children}</Box> {/* render #1 就 mount,頁面 API 立刻起跑,與 sidebar 並行 */}
</Flex>
);前提:children 的 context 是自己從 URL query 讀的(getQueryParam),
根本不需要 sidebar 那份清單 → 等它純屬白等。
為什麼平常沒感覺、只有硬重整才慢
useLoadAllPages 設了 revalidateIfStale: false:
useSWR(baseKey, fetcher, { revalidateOnFocus: false, revalidateIfStale: false, ... });session 內 cache 有資料時,再次 mount 直接 isLoading=false → children 立刻 mount。
所以 bug 只在冷 cache(硬重整 / 首次進模組)咬人。量測要用硬重整才重現得出來。
一個 hook 兩套抓法,參數只開了其中一套
useProjects 內部有兩個獨立的抓資料機制,loadAll 只控制其中一個:
// 機制 A:分頁 list(表格用,limit=10)
const swrListKey = enterpriseId && userId ? listKey : null; // ❌ 沒看 loadAll
useSWR(swrListKey, fetcher);
// 機制 B:撈全部(sidebar 用,limit=50)
const allProjectsBaseKey = loadAll && enterpriseId && userId ? ... : null;
useLoadAllPages({ baseKey: allProjectsBaseKey });sidebar 呼叫 useProjects({ loadAll: true }) 時:
- 機制 B 開 → 送
limit=50(真正要的,allProjects從這來) - 機制 A 也還開著(判斷式沒看
loadAll)→ 又送limit=10,但 sidebar 從不讀list
→ 每次 mount 白送一筆 GET /projects,還白佔一條後端 DB 連線。
修法:把互斥條件補進 key
// ✅ loadAll 的 consumer 不要開機制 A;key 為 null → useSWR 完全不送請求
const swrListKey = !loadAll && enterpriseId && userId ? listKey : null;列表頁用 useProjects({ isListPage: true })(沒 loadAll)→ 機制 A 照常 → 分頁不受影響。
Takeaways
useSWR的isLoading第一次 render 就是true(冷 cache 時),別假設一開始 false。- 別用 loading 早退包住 children;loading 往下傳給該負責的子元件,讓兄弟節點的 API 並行起跑。
- 「元件沒 mount → 它的 useSWR/useEffect 不會跑」是 gate 造成延遲的根因。
- 一個 hook 有多套抓資料機制時,開關參數要涵蓋每一套;否則會有「沒人讀卻照送」的殭屍請求。
- perf 量測要對準真實情境:有
revalidateIfStale: false的 cache,只有硬重整/冷 cache才重現得出來。