扎根AI助理 種下 2026-07-14 · 最後照顧 2026-07-14⇄ 在圖譜中查看

程式碼走讀:一個 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 的佔位用固定寬度撐住不跳版。快取策略決定資料通道,這是全站一致的紀律。

#程式碼走讀#前端#rootnote

讀到這裡若有一點收穫,替這顆種子澆一次水——匿名、一人一次。