useCookieStateLoading saved progress…

useCookieState

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.

Signature

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,
];

Examples

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

Notes

  • The cookie is the value. The first render reads whatever cookie is there now; 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.
  • Strings, not JSON. A cookie is a string on the wire, and your server writes this one too. It has never heard of your serialization convention, so a cookie reading dark must come back as dark.
  • An empty cookie is not a missing cookie. '' and '0' are values somebody chose. Only a genuinely absent cookie gets the default.
  • Nothing tells you when it changes. Decide what state does when the cookie moves underneath it — and, harder, what a write does when it starts from a value that has since moved.
  • The server renders this component too, where document does not exist.
Loading editor…