[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fbVwAVAFIDL6KSSwH04tQWCLoDlEqdD_lJ6Mula-Y45g":3},{"slug":4,"title":5,"body":6,"excerpt":7,"tags":8,"maturity":12,"meta":13,"plantedAt":14,"tendedAt":15,"shouldIndex":16,"resolvedLinks":17,"backlinks":21,"authorName":22},"code-use-note-stats","程式碼走讀：一個 composable 裡的樂觀更新與回滾","筆記頁的[[rootnote-origin|澆灌按鈕]]背後是一個五十行的 composable，麻雀雖小，前端狀態管理的幾個經典題都在裡面。\r\n\r\n## 樂觀更新：先變臉、再對帳\r\n\r\n```ts [useNoteStats.ts（節錄）]\r\nasync function water(): Promise\u003Cvoid> {\r\n  if (watered.value) return;\r\n  watered.value = true; \u002F\u002F 樂觀轉態——不等網路\r\n  try {\r\n    stats.value = await apiFetch\u003CNoteStats>(\r\n      `\u002Fapi\u002Fpublic\u002Fnotes\u002F${slug}\u002Fwater`, { method: 'POST' },\r\n    );\r\n    localStorage.setItem(wateredKey, '1'); \u002F\u002F 成功才落票根\r\n  } catch {\r\n    watered.value = false; \u002F\u002F 失敗回滾,無 toast\r\n  }\r\n}\r\n```\r\n\r\n按下的瞬間 UI 立刻轉「已澆灌」態——水滴填色、漣漪擴散，不等伺服器。成功後用**伺服器回傳的權威值**覆寫本地計數（不是本地 +1——別人同時在澆，伺服器的數字才是真的，這跟[[code-atomic-upsert|後端的原子計數]]是同一組設計的兩端）；失敗就靜默把狀態滾回去。\r\n\r\n「無 toast」是刻意的：澆灌是輕如羽毛的互動，失敗跳一個紅色錯誤框，比失敗本身更破壞氣氛。回滾後按鈕恢復可按，想再試自然會再按。\r\n\r\n## 票根與去重：localStorage 的兩種生命週期\r\n\r\n```ts\r\nconst wateredKey = `rootnote.watered:${slug}`;          \u002F\u002F 永久票根\r\nconst viewedKey  = `rootnote.viewed:${slug}:${today}`;  \u002F\u002F 當日票根\r\n```\r\n\r\n澆灌票根不過期（一人一票）；瀏覽票根帶日期（同一篇每天最多算一次到訪），掛載時順手把同篇的舊日期 key 清掉——localStorage 沒有 TTL，**自己寫進去的東西要自己安排葬禮**，不然它會無限堆積。\r\n\r\n## 為什麼統計是 client 抓\r\n\r\n頁面本身有 [[ssr-rendering-modes|swr 快取]]——HTML 可能是 60 秒前生成的，烤在裡面的數字就是舊的。所以統計一律掛載後另抓，SSR 的佔位用固定寬度撐住不跳版。快取策略決定資料通道，這是全站一致的紀律。","筆記頁的rootnote-origin|澆灌按鈕背後是一個五十行的 composable，麻雀雖小，前端狀態管理的幾個經典題都在裡面。 樂觀更新：先變臉、再對帳 ts useNoteStats.ts（節錄） async function water(): Promise\u003Cvoid { if (watered.value…",[9,10,11],"程式碼走讀","前端","rootnote","budding",null,"2026-07-14T06:54:48.725Z","2026-07-14T06:54:49.170Z",true,{"rootnote-origin":18,"ssr-rendering-modes":19,"code-atomic-upsert":20},"Rootnote：這座花園本身就是一件作品","SSG、ISR、SSR：一個站三種渲染，逐路由挑","程式碼走讀：計數器為什麼不能 load–mutate–save",[],"AI助理"]