useCookieState(key, options) is a useState whose value lives in a cookie. You use it exactly like useState, and the value rides on every request to your domain — which is the whole reason to reach for it. A theme kept in a cookie is one your server can read while it renders, so the first frame is already dark. The same preference in localStorage is invisible to the server, so it renders light, and the user watches the page change after it loads.
That reach is also the catch. A cookie is the one client-side store that something other than your component also writes: any response can come back carrying Set-Cookie, another tab can write it, a third-party script can write it. And there is no change event on document.cookie — no equivalent of the storage event that useLocalStorage leans on. Whatever your hook is holding is a copy of a value it does not own, and nothing will tell it when that copy stops being true.
Implement useCookieState(key, options). Reading and writing the cookie string is a solved problem — cookieStore covers it, and the starter hands you working readCookie / writeCookie helpers. What you are deciding here is what the hook does with them.
function useCookieState(
key: string,
options?: {
defaultValue?: string; // used only when the cookie is absent
path?: string; // defaults to '/'
maxAge?: number; // seconds
expires?: Date | number;
domain?: string;
sameSite?: 'strict' | 'lax' | 'none';
secure?: boolean;
},
): [
string | undefined,
// setState takes a value or an updater, plus options for THIS write
(next: string | undefined | ((prev: string | undefined) => string | undefined),
writeOptions?: object) => void,
];
const [theme, setTheme] = useCookieState('theme', { defaultValue: 'light' });
setTheme('dark'); // theme=dark is now on every request to this domain
A write is not just a value — it carries the cookie's attributes too. Setting undefined deletes it:
const [promo, setPromo] = useCookieState('promo', { defaultValue: 'shown' });
setPromo('dismissed', { maxAge: 60 * 60 * 24 * 30 }); // for this write: 30 days
setPromo(undefined); // cookie deleted; promo reads 'shown' again
const [cart, setCart] = useCookieState('cart', { defaultValue: '0' });
setCart((n) => String(Number(n) + 1)); // an updater, like useState
defaultValue is only for when there isn't one. An existing cookie must survive — a user who chose French three months ago should not be reset to English on page load.dark must come back as dark.'' and '0' are values somebody chose. Only a genuinely absent cookie gets the default.state does when the cookie moves underneath it — and, harder, what a write does when it starts from a value that has since moved.document does not exist.