# Practical guide

These notes explain lifecycle behavior, browser boundaries, and composition choices. Start with [README.md](README.md) for installation and runnable examples.

## Explore the live playground

[Open use-a-hook in your browser](https://use-a-hook.pages.dev/) to try five workflows before wiring them into your app. Each preview offers **View code** and expandable **How it works** notes.

- [Search and sorting](https://use-a-hook.pages.dev/#search)
- [Autosave and retry](https://use-a-hook.pages.dev/#autosave)
- [Keyboard shortcuts](https://use-a-hook.pages.dev/#shortcuts)
- [Streaming chat and scroll following](https://use-a-hook.pages.dev/#chat)
- [Saved preferences](https://use-a-hook.pages.dev/#preferences)

The [live API reference](https://use-a-hook.pages.dev/#reference) covers all 114 hooks and the context factory. Autosave and chat are labeled local simulations; real persistence and network services remain application responsibilities.

## Listing, filtering, sorting, and virtualization

```tsx
import { useCallback, useState } from 'react';
import { useSearch, useSortedList, usePagination, useCollator } from 'use-a-hook';

type Row = { id: string; name: string };
export function CustomerList({ rows }: { rows: readonly Row[] }) {
  const [query, setQuery] = useState('');
  const getText = useCallback((row: Row) => row.name, []);
  const collator = useCollator('en', { numeric: true });
  const compare = useCallback((a: Row, b: Row) => collator.compare(a.name, b.name), [collator]);
  const filtered = useSearch(rows, query, getText);
  const sorted = useSortedList(filtered, compare);
  const page = usePagination(sorted, { pageSize: 20 });
  return (
    <>
      <input value={query} onChange={event => { setQuery(event.target.value); page.reset(); }} />
      {page.items.map(row => <p key={row.id}>{row.name}</p>)}
      <button disabled={!page.hasPrevious} onClick={page.previous}>Previous</button>
      <button disabled={!page.hasNext} onClick={page.next}>Next</button>
    </>
  );
}
```

Derived hooks compare input references. Keep arrays immutable and selectors/comparators stable to retain memoization. Search indexes normalize text once while a nonempty query is active, then reuse that index for subsequent queries. The normalization is a lightweight accent/case-insensitive substring search, not a full locale-aware search engine.

`usePagination` slices already-loaded data and clamps the requested page. Reset the page explicitly when filters change. `useCursorPagination(resetKey)` resets cursor history when its key changes; the application fetches and caches pages. `useSelection` stores stable IDs, supports bulk selection in one update, and does not assume which page owns a selected row.

`useVirtualList` supports **fixed-height** items. Pass measured viewport height and scroll offset; render an outer scrolling container, a spacer of `totalHeight`, and absolutely positioned visible items at each item's `offset`. Use the underlying record ID as the React key. Dynamic row heights, sticky rows, and scroll-to-index behavior are outside this primitive. Its work is bounded by visible items plus overscan rather than total list length.

`useInfiniteScroll` observes a sentinel, locks concurrent requests, and triggers on visibility/eligibility changes. The caller owns `hasMore`, loaded pages, cancellation, and retry policy. It does not repeatedly fetch just because a sentinel remains visible. After a failed request, a new observation or an eligibility change can trigger another attempt. `useLazyValue` evaluates a pure factory only while enabled; keep the factory stable for memoization.

## Animation and high-frequency events

```tsx
const updatePosition = useRafCallback((x: number, y: number) => {
  if (elementRef.current) elementRef.current.style.transform = `translate(${x}px, ${y}px)`;
});
// Pointer handler: updatePosition(event.clientX, event.clientY)

useAnimationFrame(({ delta }) => {
  simulation.step(delta); // Milliseconds; avoid assuming a fixed frame rate.
}, running);
```

`useRafCallback` keeps only the newest arguments before the next frame. It exposes `cancel()` and `flush()`, has stable identity, and cancels on unmount. `useRafState` composes functional updates before publishing state once per frame. `useAnimationFrame` is opt-in, starts with a zero delta, resets its timing on resume, and does not update React state automatically. Browsers may pause frames in hidden tabs, so clamp or otherwise handle a large resumed delta in simulations.

Element measurements, viewport size, pointer tracking, scroll position, text selection, drag movement, and resize observation batch their updates by animation frame. `useScrollThreshold` publishes a boolean rather than every scroll offset. `useIdle` records event timestamps and maintains one timeout instead of resetting a timeout for every pointer event. `useCountdown` uses a wall-clock deadline and stops its interval on expiration.

Pass finite numeric inputs. Delays are milliseconds; negative timer delays are treated as zero. Debounce defaults to the trailing edge; optional leading/trailing controls and maxWait are described in [the reference guide](RESEARCH.md). Time-based throttling runs on the leading edge and, when needed, the trailing edge with the latest arguments. Pending scheduled callbacks cancel on delay changes and unmount. Timer callbacks use the latest committed closure without restarting their timers. Call event callbacks and schedulers from events or effects, not during render.

## Context, theme switching, and language switching

```tsx
'use client';
import { createContext } from 'react';
import { useLocalStorage, useTheme, useLocale, useRequiredContext } from 'use-a-hook';
import type { ReactNode } from 'react';
import type { ThemePreference } from 'use-a-hook';

function useSettingsState() {
  const [themeValue, setTheme] = useLocalStorage<ThemePreference>('app-theme', 'system');
  const [localeValue, setLocale] = useLocalStorage('app-locale', 'en');
  const theme = useTheme({ value: themeValue, onChange: setTheme, applyToDocument: true });
  const language = useLocale({ value: localeValue, onChange: setLocale, applyToDocument: true });
  return { theme, language };
}
const SettingsContext = createContext<ReturnType<typeof useSettingsState> | null>(null);

export function SettingsProvider({ children }: { children: ReactNode }) {
  const settings = useSettingsState();
  return <SettingsContext.Provider value={settings}>{children}</SettingsContext.Provider>;
}
export function SettingsControls() {
  const { theme, language } = useRequiredContext(SettingsContext, 'SettingsProvider');
  return (
    <>
      <button onClick={() => theme.setTheme('dark')}>Dark theme</button>
      <button onClick={() => language.setLocale('my')}>မြန်မာ</button>
    </>
  );
}
```

Theme and locale hooks own local state unless controlled. Use one settings owner/provider to share them; mounting independent hooks does not implicitly share their local state. `applyToDocument` defaults to false. When enabled, theme writes `data-theme` and `color-scheme`; locale writes `lang` and `dir`. These settings remain applied when the owner unmounts. CSS, translations, persisted preference keys, initial server settings, and any pre-hydration appearance script remain application decisions. Split contexts if different consumers should update independently.

`useRequiredContext` detects a missing provider; it does not change React context's subscription behavior. For selective subscriptions, place a stable external store in context and use `useStoreSelector` with stable subscribe/snapshot functions. The store must produce immutable, cached snapshots. Supply `getServerSnapshot` when server rendering; use an equality comparator for derived object selections.

Locale defaults to `en` rather than reading a browser preference during server rendering. `useTranslation` supports dictionary fallback and `{name}` interpolation. It does not implement ICU messages, plural rules, remote dictionary loading, or HTML injection. Intl formatter hooks reuse instances for equivalent inline option values. Pass an explicit time zone and consistent locale for deterministic server/client date output. `useRelativeTime` returns a formatter; combine it with `useNow` if labels need to advance over time.

## Async tasks, autosave, and forms

```tsx
const autosave = useAutosave(draft, (capturedDraft, signal) => persistDraft(capturedDraft, signal), {
  delay: 500
});
useUnsavedChanges(autosave.dirty);

async function publish() {
  if (await autosave.flush()) await publishSavedDraft();
}
```

Keep draft values immutable and reference-stable between edits. Autosave skips the initial value unless `saveOnMount` is true. It debounces edits, serializes writes, and drains newer queued values during `flush()`. The save callback should settle only when persistence succeeds. Rejections remain dirty and are exposed as `error`; call `flush()` to retry. Disabling stops new saves but does not roll back an active server write. Unmount aborts the current signal and cancels the timer; abort is best-effort and cannot undo writes already accepted by the server. Unmount does not flush. Conflicts, entity boundaries, account changes, and publication rules remain application responsibilities.

`useAsyncTask` takes `(signal, ...args)` and is explicitly triggered with `run()`. A new run aborts the previous signal; stale results cannot update state. Errors appear in state and `run()` resolves to `undefined` on error/cancellation, so inspect `status` rather than using an undefined result as proof of failure. `useSubmit` adds a synchronous duplicate-submit lock; tasks should honor cancellation and settle. `useRetry` is opt-in and only appropriate for idempotent work; it does not assume mutations are safe to replay. `usePolling` schedules after completion and pauses in hidden documents by default. Avoid manually calling its inherited `run()` if you need the automatic polling loop to be the sole owner of requests.

`useFetch` is a minimal JSON read hook, without caching, deduplication, pagination, or retries. URL or `enabled` changes start/disable requests. Other `RequestInit` changes apply to the next request; call `refetch()` after such changes commit. Inline options do not cause request loops. A 204 response has no data. Old data remains visible while a replacement request loads; errors clear data. Do not use mount-driven fetching for mutation side effects.

Form hooks provide values, touched fields, and synchronous validation. They do not replace a full form library's subscriptions, nested field arrays, or schema tooling. `useDirty` compares references by default; supply an equality function when needed and explicitly accept saved values with `markClean(value)`. `useHistory` stores immutable snapshots in bounded history. `useConfirm` provides state and a Promise, while the app renders the dialog. Replacing or unmounting an unanswered confirmation resolves it as false. `useUnsavedChanges` handles the browser unload prompt only; SPA route blocking requires a router adapter and browsers may suppress unload prompts.

## Browser APIs, files, and headless accessibility

Observer hooks use callback refs or explicit target elements, so they can disconnect when a target changes. `useElementRect` observes size and ancestor/window scrolling and offers `measure()` for other layout changes; it does not run an unconditional animation loop or watch every DOM mutation. Missing optional observer APIs leave initial results intact. `useMediaQuery` uses one stable browser object per mounted hook; `useOnlineStatus` shares native subscriptions across consumers. A browser online flag is not proof a server is reachable.

Event options are compared by fields. Object-ref event targets are resolved after each commit; use callback refs or state for replacements that must be observable immediately. `useClickOutside` reads its ref per event and checks composed paths. `useHotkeys` skips composing/repeated key events and editable fields by default; `mod` means Ctrl or Meta. Use key names/chords such as `mod+k`, `escape`, and `shift+arrowdown`. Alternatives, timed sequences, focus-scoping refs, application groups, and release callbacks are covered in [the hotkey guide](HOTKEYS.md). The expanded hotkey API is included in 0.3.0.

Tabs and roving-focus hooks provide keyboard/state/ARIA building blocks; apply their props and render the appropriate group roles, labels, and panels. Tabs support horizontal automatic activation. Roving focus expects a list of enabled items. `useFocusReturn` restores focus but does not trap it. Use a proven dialog primitive for focus trapping and full modal behavior. `useLockBodyScroll` reference-counts overlapping locks. Document title and favicon hooks assume one owner at a time. Drag and file-drop hooks need accessible alternative controls; set `touch-action` appropriately for a drag surface. Double-click detection delays single-click work and should not be the only way to access an action.

`useClipboard` returns false on unsupported/denied writes and reports an error. Fullscreen promises reject when unavailable or denied; catch them in the event handler. `useBroadcastChannel` follows browser same-origin/storage-partition rules and returns false from `post` when unavailable. `useImage` preloads without inserting an image element. `useScript` shares script tags by URL and security attributes and retains them for the document lifetime to avoid repeated execution. Loading a script is an explicit consumer action, not a sandbox; a failed cached script remains in error state for that key.

`useFileInput` validates file count, size, extension, and MIME before replacing the accepted list; validation failures preserve the previous accepted files. Render the actual file input, set its HTML `accept`/`multiple` attributes, and attach the returned ref and change handler. Client-side checks do not replace server validation. `useObjectUrl` revokes URLs on replacement/unmount. File upload transport, retries, progress, and ownership can be composed with async task hooks.

## Persistent state and server rendering

Storage uses JSON serialization by default. Dates then return as strings; functions, undefined, cyclic values, and class instances are unsuitable for that default. Supply matching options.serializer functions for other formats. Generics describe the expected shape but do not validate stored data. Initial values are captured once per mount. Use consistent defaults across consumers of the same key. No writes happen until the setter is called.

Storage snapshots are raw strings and parsed values are memoized. Functional setters read current storage so same-tab batched updates compose; identical serialized values skip writes/notifications. Hook writes notify same-document hook consumers, and browser storage events cover applicable other documents. Direct same-document `localStorage.setItem()` calls are not automatically observed. Cross-tab read/modify/write operations are not atomic. Session storage follows browser tab boundaries. Read/parse failures use the initial value. By default, write/remove failures call `onError`, or throw when no handler exists. Opt into `fallbackToMemory` to keep shared same-document state after persistence failures; that state does not survive reloads or synchronize across tabs. `defaultServerValue` and `storageSync` provide explicit hydration and cross-document controls. Failed writes do not pretend to persist. Storage and serialization are synchronous: keep values small.

Browser state uses deterministic server defaults and refreshes after hydration. Supply matching server snapshots/defaults when the initial value matters. `useNow` defaults to timestamp zero for the first render; pass a serialized server timestamp for visible clock output. No hook reads browser state or starts subscriptions merely because it was imported. Hook modules preserve `use client`; hook consumers still belong below a client boundary in React Server Component applications. Pure initializer factories must tolerate React Strict Mode and abandoned renders.

The native `useQueryParams` adapter observes hook-owned updates and `popstate`; third-party history writes are not patched. Inject a router-aware adapter in Next.js or other frameworks so router caches and navigation stay consistent. Returned URLSearchParams are a read view; use the setter to change state. `useHash` follows native fragment navigation.

## Development and verification

```sh
npm ci
npm run check
npm pack
```

The check builds both formats, validates type inference, rejected inputs, and all 115 complete TSX examples, runs behavior tests plus a browser/SSR mount scenario for every hook, verifies the exact catalog/export map, measures production consumer bundles with React externalized, and loads every root/subpath export from an actual packed artifact in ESM and CommonJS.

Gzip bundle budgets are 3,072 bytes for `useHotkeys` (root and subpath), 1,024 bytes for `useToggle`, 1,536 bytes for `useRafCallback`, 2,048 bytes for the filter/sort/pagination group, and 24,576 bytes when intentionally importing all 114 hooks and the context utility. Single-hook budgets remain independent of catalog size. These measure generated consumer code, not runtime speed or npm download size. Runtime tests check work counts and invariants rather than fragile machine-specific timing limits.

Run the tests against another already-installed React/React DOM pair:

```sh
USE_A_HOOK_REACT_ROOT=/path/to/consumer npm test
```

The package declares React 18/19 peer support; the release CI matrix is configured for both runtime majors. All hooks have Strict Mode mount/unmount and server-rendering smoke scenarios. Focused tests cover behavior and important error/cleanup paths; passing these is not a claim of exhaustive browser, accessibility, or branch coverage. Modern browser APIs and Intl are used directly; no polyfills or translation bundles are included.

Public API signatures, stories, and examples in `API.md` and the catalog are refreshed by the build. Edit `catalog.json` for the explanation and `examples/<hook-name>.tsx` for its code; `npm run test:examples` type-checks every example. Contributions must add a concrete use case, an independently importable entry, types, meaningful behavior tests, a catalog smoke case, and appropriate size checks. Do not add aliases merely to increase the hook count.

Design references: [React external stores](https://react.dev/reference/react/useSyncExternalStore), [effects and cleanup](https://react.dev/reference/react/useEffect), [animation frames](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame), and [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl).

Publishing remains a separate, authenticated action:

```sh
npm publish --access public
```

The source upgrades from the [reference study](RESEARCH.md) preserve existing defaults while adding explicit options. The guide documents serialization/fallback behavior, frame sharing, timer semantics, and API differences from the studied packages.

`npm run test:consumer-types` checks the built declarations and all examples in an isolated consumer. Set `USE_A_HOOK_TYPES_ROOT` to a project containing React, React type declarations, and TypeScript to check another supported major. CI performs this consumer check with React 19 types in addition to the React 18 build types.

The [expanded reference study](RESEARCH.md#expanded-study-25-additional-packages) maps all 43 requested packages to implemented or deliberately retained APIs. New source includes elapsed-time/count-up animation, container breakpoints, error recovery, cookies, screenshot orchestration, portal hosts, and selector-based context state. These changes are included in 0.3.0.
