2026年7月22日
React useCookie Hook:把 Cookie 變成響應式狀態(2026)
一個主題切換按鈕把使用者的選擇存進 cookie,好讓伺服器在下一次請求時直接渲染出正確的主題——不閃一下錯誤的模式。元件寫下 document.cookie = 'theme=dark',然後……什麼都沒有重新渲染。document.cookie 不是 React 狀態:寫入不會通知任何人,讀取意味著解析一串分號分隔的字串,而且它變化時沒有任何事件可以訂閱。每個關心這個 cookie 的元件,都在安靜地讀著一份過期的副本。
@reactuses/core 的 useCookie 把 cookie 變成普通的元件狀態:像狀態一樣讀、像狀態一樣寫,同一標籤頁裡監聽同一個 key 的所有實例一起更新。它構建在 js-cookie 之上,屬性處理(過期時間、路徑、SameSite)是久經考驗的那種。以下都是真實 API,TypeScript 優先。
手寫版本,以及它在哪裡散架
Cookie 比你用過的每一個框架都古老,它的 API 也毫不掩飾這一點。手寫的 React 版本長這樣:
function ThemeToggle() {
const [theme, setTheme] = useState(() =>
document.cookie
.split('; ')
.find((row) => row.startsWith('theme='))
?.split('=')[1] ?? 'light'
);
const update = (next: string) => {
document.cookie = `theme=${next}; path=/; max-age=31536000`;
setTheme(next);
};
// ...
}
它散架的幾個常見位置:
- 字串解析是你的問題。 按
'; '切分、前綴匹配 key、解碼值——這是一個你現在要在每個元件裡維護的 cookie 解析器。 - 別的地方不會更新。 兩個顯示同一個 cookie 的元件各自握著自己的
useState副本。一個寫入了;另一個繼續渲染舊值,直到某個無關的更新碰巧讓它重渲染。 - 屬性全靠字串拼接。
path、expires、secure、SameSite都是手工拼接的片段——拼錯了不會報錯,只會悄悄產出一個作用域錯誤的 cookie。 - 在伺服器端直接崩潰。 SSR 期間不存在
document,就算加了守衛,伺服器端渲染和客戶端首次渲染也可能不一致——hydration 不匹配。
useCookie——Cookie 即狀態
import { useCookie } from '@reactuses/core';
function ThemeToggle() {
const [theme, setTheme] = useCookie('theme', { expires: 365, path: '/' }, 'light');
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
當前主題:{theme}
</button>
);
}
簽名:
function useCookie(
key: string,
options?: Cookies.CookieAttributes,
defaultValue?: string
): readonly [
string | undefined, // 當前值
(value: string | undefined | ((prev) => string | undefined)) => void, // 更新
() => void // 刷新
];
三點值得注意:
- 值是字串。 Cookie 本質是字串傳輸——這個 hook 不替你猜序列化方式。要存物件?自己
JSON.stringify,或者先想想它是否真的該放在 cookie 裡(每個 cookie 約 4KB 預算,而且每個位元組都會跟著每次 HTTP 請求上路)。 - 設為
undefined就是刪除。setTheme(undefined)直接移除 cookie——不需要額外匯入一個remove函數。函數式更新也支援:setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'))。 - 掛載時 cookie 不存在,預設值會被寫入。 傳
'light'作為預設值,首次渲染時 cookie 就會以theme=light的形式落地——這意味著伺服器在下一次請求就能看到它。對主題 cookie 來說,這正是目的所在。
Cookie 屬性——有型別,不拼接
options 參數原樣透傳給 js-cookie,所以就是完整的 Cookies.CookieAttributes:
| 屬性 | 作用 |
|---|---|
expires | 從現在起的天數(365),或一個精確時刻的 Date。省略則是隨瀏覽器關閉消失的會話 cookie |
path | 哪些路徑能看到這個 cookie——幾乎總是想要 '/' |
domain | 跨子網域共享('.example.com') |
secure | 僅 HTTPS |
sameSite | 'strict'、'lax' 或 'none'——跨站發送策略 |
options 物件按值比較,不按引用——每次渲染都內聯傳 { expires: 365, path: '/' } 完全沒問題,不會引起任何抖動。
一個值得知道的鋒利邊緣:屬性是寫入時的配置。瀏覽器不允許 JavaScript 讀回一個 cookie 的 path 或過期時間——所以刪除走的是你寫入時用的同一組 path/domain。同一個 key 的 options 保持一致,這一點就永遠咬不到你。
同步模型:同標籤頁、其他標籤頁、以及伺服器
這是 cookie 與 Web Storage 真正不同的地方,這個 hook 對此很誠實。
同標籤頁:自動。 標籤頁裡每一個 useCookie('theme', …) 實例都會在任何一個寫入時一起更新。Cookie 沒有原生的變化事件,所以 hook 在寫入時派發一個內部 window 事件——兄弟元件保持同步,你什麼都不用接。
其他標籤頁:不自動。 localStorage 有跨標籤頁的 storage 事件;cookie 什麼都沒有。別的標籤頁寫了 cookie,這個標籤頁自己不會知道。跨標籤頁的偏好同步是 useLocalStorage 的領域——完整工具箱見《React 跨標籤頁狀態》。
伺服器(或任何外部寫入):refreshCookie。 Cookie 的超能力是伺服器也能寫它——比如 fetch 響應裡的 Set-Cookie 標頭。這種寫入同樣不觸發任何客戶端事件,所以元組的第三個元素用來按需重讀:
const [session, , refreshSession] = useCookie('session_hint', {}, '');
const login = async (creds: Credentials) => {
await fetch('/api/login', { method: 'POST', body: JSON.stringify(creds) });
refreshSession(); // 拿到響應剛剛設定的 cookie
};
心智模型一行一個:同標籤頁寫入自我傳播;跨標籤頁交給 localStorage;外部寫入需要 refreshCookie()。
useCookie vs useLocalStorage vs useSessionStorage
三者都讓持久化的值變成響應式;區別在於誰能看到這個值、能看多久:
useCookie | useLocalStorage | useSessionStorage | |
|---|---|---|---|
| 伺服器能看到 | ✅ 每次請求都帶上 | ❌ | ❌ |
| 生命週期 | 你來定(expires) | 永久直到清除 | 標籤頁關閉 |
| 跨標籤頁同步 | ❌(無原生事件) | ✅ | ❌ |
| 容量預算 | 約 4KB,每次請求都發送 | 約 5MB,留在本地 | 約 5MB,留在本地 |
| 值型別 | string | 任意(型別化序列化器) | 任意(型別化序列化器) |
決策規則一句話:伺服器需要這個值才能正確渲染嗎? 主題、語言、同意狀態、A/B 分桶——答案是「是」的,就放 cookie,因為伺服器在發出第一個位元組的 HTML 之前就能從請求標頭裡讀到它。如果值只屬於客戶端——表單草稿、面板位置、伺服器從不渲染的快取偏好——Web Storage 更寬敞,還能跨標籤頁同步。儲存那一側的完整指南見《React 裡的 useLocalStorage》。
(把房間裡的大象說明白:真正的鑑權 token 屬於 HttpOnly cookie,JavaScript——包括這個 hook——根本讀不到它。這是特性,不是缺陷。useCookie 面向的是可讀層:偏好、提示、開關。)
真實使用場景
- 不閃爍的主題切換。 主題 cookie 隨請求上行,伺服器直接渲染
<html class="dark">,客戶端不需要任何修正。這個場景用localStorage不可能實現——伺服器永遠看不到 storage。 - 語言選擇。 同樣的形狀:使用者選了語言,cookie 持久化,伺服器端渲染讀到它,從第一個位元組開始就用正確的語言響應。
- Cookie 同意橫幅。 用長
expires寫下同意決定;客戶端程式碼和伺服器端中介軟體都能在載入分析腳本前檢查它。 - A/B 實驗分桶。 用函數式更新只分配一次(
setBucket((prev) => prev ?? assignBucket())),分桶結果對伺服器端渲染、邊緣中介軟體和客戶端同時可見。 - 登入後的 UI 提示。 一個非敏感的
logged_in=1提示 cookie(由伺服器隨真正的HttpOnly會話一起設定),讓客戶端立刻渲染帳戶相關的介面——登入呼叫之後refreshCookie()把它撿起來。
SSR:defaultValue 規則
伺服器端渲染期間沒有 document.cookie,hook 什麼都讀不到。規則就一句話:做 SSR 時,永遠傳 defaultValue。 伺服器渲染預設值,客戶端首次渲染產出相同的標記(hydration 要求的正是這個——React 會比對兩者),真正的 cookie 值緊接著在 effect 裡落地。SSR 應用裡省略預設值,hook 會在開發環境警告你,因為伺服器(渲染空)和客戶端(渲染 cookie)會不一致——hydration 不匹配。
如果你的框架能在伺服器端讀 cookie(Next.js 的 cookies()、Remix 的 loader),還可以更進一步:把真實的請求 cookie 作為 defaultValue 傳進來,首屏直接正確,不需要任何 hydration 後的修正。更宏觀的模式——為什麼所有瀏覽器 API 都需要這種紀律——見《SSR 安全的 React Hooks》。
要點回顧
document.cookie不是狀態——沒有響應性、要解析字串、屬性靠手拼。useCookie把它變成js-cookie加持的[value, set, refresh]元組。- 設
undefined即刪除;支援函數式更新; cookie 缺失時會用你的defaultValue初始化,伺服器下一次請求就能看到。 - 記住同步模型: 同標籤頁實例自動同步;跨標籤頁不行(那是
useLocalStorage的事);伺服器寫入的 cookie 需要顯式refreshCookie()。 - 按受眾選擇: 伺服器渲染需要的值——主題、語言、同意、A/B——放 cookie。純客戶端資料放 Web Storage。
- SSR 規則: 伺服器端渲染時永遠傳
defaultValue——更好的做法是把真實的請求 cookie 作為預設值傳入。
從 @reactuses/core 拿來用,讓你的 cookie 變成它一直想成為的那種狀態。