程式碼走讀:一個 composable 裡的樂觀更新與回滾
筆記頁的澆灌按鈕背後是一個五十行的 composable,麻雀雖小,前端狀態管理的幾個經典題都在裡面。
樂觀更新:先變臉、再對帳
useNoteStats.ts(節錄)ts
async function water(): Promise<void> {
if (watered.value) return;
watered.value = true; // 樂觀轉態——不等網路
try {
stats.value = await apiFetch<NoteStats>(
`/api/public/notes/${slug}/water`, { method: 'POST' },
);
localStorage.setItem(wateredKey, '1'); // 成功才落票根
} catch {
watered.value = false; // 失敗回滾,無 toast
}
}
按下的瞬間 UI 立刻轉「已澆灌」態——水滴填色、漣漪擴散,不等伺服器。成功後用伺服器回傳的權威值覆寫本地計數(不是本地 +1——別人同時在澆,伺服器的數字才是真的,這跟後端的原子計數是同一組設計的兩端);失敗就靜默把狀態滾回去。
「無 toast」是刻意的:澆灌是輕如羽毛的互動,失敗跳一個紅色錯誤框,比失敗本身更破壞氣氛。回滾後按鈕恢復可按,想再試自然會再按。
票根與去重:localStorage 的兩種生命週期
ts
const wateredKey = `rootnote.watered:${slug}`; // 永久票根
const viewedKey = `rootnote.viewed:${slug}:${today}`; // 當日票根
澆灌票根不過期(一人一票);瀏覽票根帶日期(同一篇每天最多算一次到訪),掛載時順手把同篇的舊日期 key 清掉——localStorage 沒有 TTL,自己寫進去的東西要自己安排葬禮,不然它會無限堆積。
為什麼統計是 client 抓
頁面本身有 swr 快取——HTML 可能是 60 秒前生成的,烤在裡面的數字就是舊的。所以統計一律掛載後另抓,SSR 的佔位用固定寬度撐住不跳版。快取策略決定資料通道,這是全站一致的紀律。
讀到這裡若有一點收穫,替這顆種子澆一次水——匿名、一人一次。