npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-stateful-hooks

v0.2.1

Published

A small, well-typed collection of SSR-safe React hooks for browser state: persisted storage with cross-tab sync, debounced values, and media queries.

Readme

react-stateful-hooks

SSR-first React hooks for browser state — zero hydration mismatches, built on useSyncExternalStore.

npm version npm downloads bundle size CI codecov types license docs

A small, well-typed, SSR-safe collection of React hooks for browser state. Every hook is built on React's official useSyncExternalStore, so it renders a stable value on the server and hydrates without a mismatch — the thing most hook libraries get wrong. Tree-shakeable, ~2 kB gzipped, ships ESM + CJS + types, and its only runtime dependency is the use-sync-external-store shim (for React 17)

The problem: every project re-implements "persist this bit of state to localStorage" — and most versions break under SSR, crash on corrupted JSON, or silently drift out of sync between tabs. This library does it once, correctly

📖 Documentation & live demos → — every hook with an interactive example (English / Русский).

Hooks at a glance

| Hook | What it does | | ----------------------------------------------------- | ------------------------------------------ | | useLocalStorageState | Persisted state with cross-tab sync | | useSessionStorageState | Per-tab persisted state | | useDebouncedValue | Trailing-edge debounce of a value | | useMediaQuery | Reactive, SSR-safe CSS media query | | useNetworkState | Reactive, SSR-safe online/offline status | | useCopyToClipboard | Copy with auto-resetting "copied" feedback | | usePrefersColorScheme | Reactive 'light' \| 'dark' preference | | usePrefersReducedMotion | Reactive reduced-motion preference | | useHydrated | true after hydration, mismatch-free | | useCookieState | Cookie-backed state, flash-free under SSR |

Why react-stateful-hooks?

The hooks space is crowded — here is where this library is deliberately different. The focus is correctness under SSR and across tabs, not breadth.

| | react-stateful-hooks | usehooks-ts | react-use | | -------------------------------------- | :-------------------: | :---------: | :-----------------: | | SSR-safe via useSyncExternalStore | ✅ every hook | ⚠️ partial | ⚠️ partial / legacy | | Cross-tab storage sync | ✅ built in | ❌ | ⚠️ | | Survives corrupted JSON / quota errors | ✅ falls back | ❌ | ❌ | | Same-tab sync across components | ✅ | ⚠️ | ⚠️ | | Bundle size | ~2 kB, tree-shakeable | small | large | | Runtime dependencies | 1 (official shim) | 0 | many | | TypeScript-first | ✅ | ✅ | ⚠️ |

If you need hundreds of hooks, reach for react-use. If you want a handful of browser-state hooks that behave correctly in Next.js / Remix and across tabs, this is for you.

Install

npm install react-stateful-hooks

react >= 17 is a peer dependency

useLocalStorageState

A drop-in useState that persists to localStorage and stays in sync across browser tabs

import { useLocalStorageState } from 'react-stateful-hooks';

function ThemeToggle() {
  const [theme, setTheme, resetTheme] = useLocalStorageState('theme', 'light');

  return (
    <>
      <button
        onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}
      >
        Theme: {theme}
      </button>

      <button onClick={resetTheme}>Reset</button>
    </>
  );
}

Signature

const [value, setValue, removeValue] = useLocalStorageState<T>(
  key: string,
  defaultValue: T,
  options?: {
    serializer?: { parse(raw: string): T; stringify(value: T): string };
    syncTabs?: boolean; // default: true
  },
);

| Return | Description | | ------------- | ------------------------------------------------------------------- | | value | Current value (typed as T) | | setValue | Accepts a value or an updater (prev) => next, like useState | | removeValue | Clears the key from storage and resets state to defaultValue |

Behaviour worth knowing

  • SSR-safe — built on useSyncExternalStore, so it returns defaultValue on the server and hydrates without a mismatch, then reads storage on the client
  • Resilient — corrupted JSON or a getItem/setItem failure (quota, private mode) falls back to the default and keeps the in-memory value instead of throwing
  • Cross-tab sync — listens to the storage event and updates state when another tab writes the same key. Disable with { syncTabs: false }. Hooks in the same tab always stay in sync, regardless of this flag
  • Custom serialization — pass a serializer to support Date, Map, BigInt, or a compact wire format
const [since, setSince] = useLocalStorageState('since', new Date(), {
  serializer: {
    parse: (raw) => new Date(JSON.parse(raw)),
    stringify: (value) => JSON.stringify(value.getTime()),
  },
});

useSessionStorageState

Same API and guarantees as useLocalStorageState, but backed by sessionStorage (state lives until the tab closes). Ideal for wizard steps, scroll positions, or any throwaway-per-session state

const [step, setStep] = useSessionStorageState('wizard:step', 0);

useDebouncedValue

Returns a debounced copy of a value that only updates after the delay passes without further changes — rapid updates collapse into a single trailing update

const [query, setQuery] = useState('');
const debouncedQuery = useDebouncedValue(query, 300);

useEffect(() => {
  search(debouncedQuery);
}, [debouncedQuery]);

useMediaQuery

Tracks whether a CSS media query matches and re-renders on change. SSR-safe — returns defaultState (default false) on the server

const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
const isWide = useMediaQuery('(min-width: 1024px)');

useNetworkState

Tracks the browser's online/offline status. SSR-safe — returns { online: defaultOnline } (default true) on the server. since is the time of the last status change, or undefined until the first transition

const { online, since } = useNetworkState();

if (!online) return <Banner>You are offline.</Banner>;
const { online, since } = useNetworkState(defaultOnline?: boolean); // default: true

useCopyToClipboard

Returns a copy function plus the state of the last copy attempt. Uses the async Clipboard API (requires a secure context); when it's unavailable, copy resolves false and records an error instead of throwing. copied flips back to false after resetDelay ms so "Copied!" feedback needs no manual timer

const [copy, { copied }] = useCopyToClipboard();

<button onClick={() => copy(url)}>{copied ? 'Copied!' : 'Copy link'}</button>;
const [copy, { value, error, copied }] = useCopyToClipboard(options?: {
  resetDelay?: number; // ms until `copied` resets; 0 = never. default: 2000
});

usePrefersColorScheme

Tracks the user's preferred color scheme via (prefers-color-scheme: dark) SSR-safe — returns defaultScheme (default 'light') on the server

const scheme = usePrefersColorScheme(); // 'light' | 'dark'

return <div data-theme={scheme} />;
const scheme = usePrefersColorScheme(defaultScheme?: 'light' | 'dark'); // default: 'light'

usePrefersReducedMotion

Tracks whether the user has requested reduced motion via (prefers-reduced-motion: reduce). SSR-safe — returns defaultValue (default false) on the server

const reduceMotion = usePrefersReducedMotion();

<motion.div animate={reduceMotion ? undefined : { x: 100 }} />;

useHydrated

Returns false on the server and during the first client render, then true once hydrated. Built on useSyncExternalStore, so the first client render matches the server markup — no hydration mismatch — then flips to true after commit. Use it to gate browser-only output

const hydrated = useHydrated();

// Renders the same thing on both sides first, then the client-only value
return <span>{hydrated ? new Date().toLocaleTimeString() : null}</span>;

useCookieState

A useState that persists to document.cookie. Unlike localStorage, cookies are sent with every request, so the value can be read on the server and passed via serverValue for a flash-free SSR render (ideal for theme/locale)

const [theme, setTheme, clearTheme] = useCookieState('theme', 'light');

Signature

const [value, setValue, removeValue] = useCookieState<T>(
  name: string,
  defaultValue: T,
  options?: {
    serializer?: { parse(raw: string): T; stringify(value: T): string };
    serverValue?: string | null; // raw cookie read on the server (SSR)
    path?: string; // default: '/'
    domain?: string;
    maxAge?: number; // seconds
    expires?: Date;
    sameSite?: 'lax' | 'strict' | 'none'; // default: 'lax' ('none' implies secure)
    secure?: boolean;
  },
);
  • SSR-safe — returns serverValue (parsed) or defaultValue on the server, then reads the cookie on the client. In Next.js, pass the request cookie so the value renders during SSR instead of flashing the default
  • Resilient — a corrupted cookie value falls back to the default
  • Same-tab sync — hooks bound to the same cookie name stay in sync. Cookies have no cross-tab change event, so other tabs update on their next render
// Next.js (App Router): no flash of the wrong theme on first paint
const [theme, setTheme] = useCookieState('theme', 'light', {
  maxAge: 60 * 60 * 24 * 365,
  serverValue: cookies().get('theme')?.value,
});

getColorSchemeScript

A helper (not a hook) that returns a tiny blocking script to run before first paint, so the page never flashes the wrong theme. It reads the stored choice (from useLocalStorageState or useCookieState), falls back to the OS preference, and sets a light/dark class on <html>

// Next.js App Router — app/layout.tsx
import { getColorSchemeScript } from 'react-stateful-hooks';

<head>
  <script dangerouslySetInnerHTML={{ __html: getColorSchemeScript() }} />
</head>;
getColorSchemeScript(options?: {
  key?: string; // storage key, default: 'theme'
  storage?: 'localStorage' | 'cookie'; // default: 'localStorage'
  attribute?: string; // 'class' (default) or e.g. 'data-theme'
  defaultScheme?: 'light' | 'dark' | 'system'; // default: 'system'
});

See the full no-flash dark mode recipe in the docs.

Development

npm install
npm test          # Vitest + Testing Library (jsdom)
npm run lint
npm run typecheck
npm run build     # ESM + CJS + .d.ts via Vite library mode

Contributing

Issues and PRs are welcome — see CONTRIBUTING.md for the workflow and conventions. Changes are tracked in CHANGELOG.md.

License

MIT © Evgenii Pokalyuk