# Reference study and hook upgrades

See these implementation choices in the [live playground](https://use-a-hook.pages.dev/), which offers five interactive workflows and a [searchable API reference](https://use-a-hook.pages.dev/#reference) for all 115 exports.

This source update studies the published packages listed below. Exact versions, repository links, declarations, README examples, source (when shipped), and licenses were downloaded from the npm registry. No reference package code was executed or added as a runtime dependency. The implementations are independent; no upstream source was copied.

The expanded development catalog contains **114 hooks and one context utility**. These upgrades and the earlier hotkey work are included in 0.3.0.

| Reference | Version / license | Our API | Result |
| --- | --- | --- | --- |
| [react-use-measure](https://github.com/pmndrs/react-use-measure) | 2.1.7 / MIT | useElementRect | Added optional scroll tracking, independent scroll/resize debounce, offset dimensions, orientation/visual-viewport invalidation, injected observer support, and explicit disabling. |
| [use-composed-ref](https://github.com/Andarist/use-composed-ref) | 1.4.0 / MIT | useComposedRef (new) | Combine object and callback refs, deduplicate repeated refs, clear on detach, and run cleanup returned by React 19 callback refs. |
| [use-latest](https://github.com/Andarist/use-latest) | 1.3.0 / MIT | useLatest | Retained commit-phase refresh and stable ref identity. Added a regression test proving an abandoned render cannot replace a committed value. |
| [use-debounce](https://github.com/xnimorz/use-debounce) | 10.1.1 / MIT | useDebouncedCallback / useDebouncedValue | Added leading/trailing edges, maxWait, pending inspection, structural value equality, and inert retained schedulers after cleanup. |
| [use-stick-to-bottom](https://github.com/samdenty/use-stick-to-bottom) | 1.1.6 / MIT | useStickToBottom (new) | Added event-driven following with user scroll-away escape, explicit resume/stop, content observation, and reduced-motion-aware native scrolling. |
| [use-local-storage-state](https://github.com/astoilkov/use-local-storage-state) | 20.0.0 / MIT | useLocalStorage / useSessionStorage | Added custom serializers, server fallback values, cross-document sync control, and opt-in shared memory fallback while retaining existing error defaults. |
| [use-device-pixel-ratio](https://github.com/rexxars/use-device-pixel-ratio) | 1.1.2 / MIT | useDevicePixelRatio (new) | Added subscription sharing, raw-resolution query re-arming, legacy media-listener support, fractional ratios, and optional output rounding/capping. |
| [@react-hook/resize-observer](https://github.com/jaredLunde/react-hook) | 2.0.2 / MIT | useResizeObserver | Added shared observers per window/constructor/box, latest-entry batching, independent target cleanup, rounding, callback-only use, box selection, and injected constructors. |
| [react-tracked](https://github.com/dai-shi/react-tracked) | 2.0.1 / MIT | useStoreSelector | Retained explicit selectors rather than adding proxy tracking. Equal selected objects now survive inline-selector/parent renders; failed comparisons cannot poison the source cache. |
| [react-window-infinite-loader](https://github.com/bvaughn/react-window-infinite-loader) | 2.0.1 / MIT | useInfiniteLoader (new) | Added inclusive visible-range loading, minimum batches, prefetch thresholds, one in-flight request, completed-range coalescing, cancellation, and explicit retry/reset. |
| [@react-hook/throttle](https://github.com/jaredLunde/react-hook) | 2.2.0 / MIT | useThrottledCallback / useThrottledValue | Added configurable leading/trailing edges, pending inspection, cancellation, and stale-call protection. Our delay remains milliseconds rather than FPS. |
| [use-image](https://github.com/konvajs/use-image) | 1.1.4 / MIT | useImage | Added optional decoding, referrer policy set before src, loaded image access, cached-load handling, and stale-request/decode guards. |
| [@react-hook/debounce](https://github.com/jaredLunde/react-hook) | 4.0.0 / MIT | useDebouncedCallback / useDebouncedValue | Used the same independent scheduling implementation rather than introducing duplicate hooks or extra state-setter aliases. |
| [@react-hook/event](https://github.com/jaredLunde/react-hook) | 1.2.6 / MIT | useEventListener | Added object-ref targets and SVG typing; target replacement is checked after commit without reconnecting equal subscriptions. |
| [@react-hook/window-size](https://github.com/jaredLunde/react-hook) | 3.1.1 / MIT | useWindowSize | Added initial dimensions, enablement, orientation handling, and visual-viewport selection with shared frame-batched subscriptions. |
| [@heroui/use-callback-ref](https://github.com/heroui-inc/heroui) | 2.1.8 / MIT | useEventCallback | Retained the existing stable, commit-refreshed callback API. Added abandoned-render regression coverage instead of another alias. |
| [@heroui/use-viewport-size](https://github.com/heroui-inc/heroui) | 2.0.1 / MIT | useViewportSize (new) | Added visual viewport dimensions plus offsets/scale, layout fallback, deterministic hydration values, shared subscriptions, and frame coalescing. |
| [react-timer-hook](https://github.com/amrlabib/react-timer-hook) | 4.0.9 / ISC | useTimer / useStopwatch (new); useCountdown / useNow retained | Added duration pause/resume/restart, once-per-generation expiry, elapsed-time controls, and display parts. Existing absolute-deadline and wall-clock hooks remain available. |

References in the first table report MIT licensing except `react-timer-hook`, which reports ISC. The timer reference here is the package maintained at `amrlabib/react-timer-hook`, rather than the similarly named scoped package. The joined infinite-loader/throttle URL in the request was interpreted as two references.

## Measurement: share the work, keep the boxes honest

```tsx
const ownRef = useRef<HTMLDivElement>(null);
const size = useResizeObserver<HTMLDivElement>({
  box: 'border-box',
  round: Math.round,
});
const ref = useComposedRef<HTMLDivElement>(ownRef, size.ref);

return <div ref={ref}>Width: {size.width ?? 'measuring…'}</div>;
```

Full component: [useComposedRef.tsx](examples/useComposedRef.tsx). Imports and setup are included there.

Observers are created on subscription, never at import or during server rendering. Subscribers share one native observer and one scheduled frame per window/constructor/box. A later entry replaces an earlier entry for the same target in a frame. Removing one target leaves other subscriptions active; removing the last cancels queued work and disconnects. Different observation boxes have separate pools. Constructor/observe errors surface to the application rather than silently changing the requested box.

`useResizeObserver` preserves content-box fractional CSS dimensions by default. `border-box` uses native box sizes when available and falls back to contentRect if unavailable. Vertical writing modes map inline/block dimensions to physical width/height. Device-pixel-content-box uses native device-pixel dimensions or approximates them from contentRect × DPR. `updateState: false` is callback-only; dimensions retain their last published value. Updating callback/rounding functions does not reconnect the observer. The next observer notification uses the latest committed functions.

`useElementRect({ offsetSize: true })` replaces width/height with offset dimensions when available, while position/edges stay viewport-relative. It is useful for measuring layout size under CSS scale transforms; do not interpret all eight returned fields as a single untransformed rectangle. Measurements are frame-batched; optional debounce delays precede that frame. Nested scroll tracking is on by default to preserve the existing API.

## Scheduling: edges are a product decision

```tsx
const preview = useDebouncedCallback(updatePreview, 250, {
  leading: true, trailing: true, maxWait: 1000,
});
preview(nextDraft);
if (preview.isPending()) preview.flush();
```

Full components: [debounce](examples/useDebouncedCallback.tsx), [throttle](examples/useThrottledCallback.tsx).

Debounce defaults to trailing-only; throttle defaults to both edges. A leading debounce call is not duplicated on the trailing edge unless another call arrives. Both edges false schedules no work. Debounce maxWait is clamped to at least the quiet period. Throttle windows use elapsed time, not display FPS, and handle a backward clock jump as a new window. Methods remain stable for equivalent scalar options and read the latest committed callback.

Scheduler calls still return void to preserve our API; `flush()` returns the callback result when it actually runs pending work. `isPending()` is an imperative inspection method, not reactive state. Cleanup cancels pending work and makes a retained scheduler inert. We do not flush persistent side effects automatically on unmount or page exit. Value hooks still return a value, with scheduling options added; value debounce also accepts `equalityFn`. `useRafCallback` remains the separate choice for display-frame work.

## Persistence: explicit fallback, predictable hydration

```tsx
const [visits, setVisits, remove] = useLocalStorage('visits', BigInt(0), {
  serializer: { parse: BigInt, stringify: String },
  defaultServerValue: BigInt(0),
  fallbackToMemory: true,
  onError: reportPersistenceFailure,
});
```

Every consumer of a key must agree on its default, serialization, and fallback policy. Read/parse failures retain the captured initial value. Serialization and write/remove failures continue to throw unless `onError` handles them. `fallbackToMemory: true` opts into shared same-document memory state after write/remove failures; it does not persist through reloads or synchronize fallback state across tabs. A successful later write clears the memory override. Serialization failures do not install invalid fallback values. Memory storage is never used on the server.

`defaultServerValue` controls server rendering and initial hydration; the browser snapshot is then read without a hydration mismatch. `storageSync: false` disables cross-document storage events while retaining same-document hook synchronization. Direct calls to localStorage.setItem from unrelated code do not notify same-tab hooks; use the returned setter or dispatch an appropriate notification. There is no new isPersistent tuple position: the original three-item return contract is preserved.

## Viewports and pixels: two different rulers

- [Visual viewport example](examples/useViewportSize.tsx): mobile keyboard/zoom dimensions, offsets, and scale. It falls back to the layout viewport when VisualViewport is unavailable.
- [Canvas DPR example](examples/useDevicePixelRatio.tsx): fractional device pixel ratio, optional rounding/capping, and raw media-query re-arming even when the capped result stays unchanged.

`useWindowSize()` keeps layout viewport defaults and undefined server dimensions. Optional initial dimensions also apply before the first client measurement frame. Disabled hooks return those fallback values. Viewport consumers share event subscriptions and one frame per mode. Dimension-only consumers skip offset-only updates. DPR consumers share subscriptions and allocate no polling interval. Raw-ratio changes re-arm the query without rendering capped/rounded consumers whose returned value stays equal. DPR rounding is nearest-integer and defaults off; this differs from the studied package’s default floor/cap behavior.

## Lists: load the window, not the universe

[Complete virtual audit-log example](examples/useInfiniteLoader.tsx) combines useVirtualList and useInfiniteLoader. It requires `/api/audit-rows?start=...&stop=...` to return `{ index, label }[]` for the requested inclusive batch. The demo has 1,000 total rows; the hook can also handle much larger rowCount values without scanning the entire dataset.

Ranges are zero-based/inclusive. Prefetch threshold expands the visible window. Minimum batches stop at loaded boundaries and dataset ends. Requests are serialized, and overlapping notifications do not duplicate pending work. Successful ranges are cached and coalesced until reset; resolve loadMoreRows only when a batch has actually been handled. Loading empty/partial results as success can leave missing data covered by that cache: reset after eviction or change resetKey with the dataset.

A failure pauses automatic loading until retry/reset. Disabling, reset, dataset replacement, and unmount abort the active signal and prevent stale hook-state publication. Your loader must honor AbortSignal and guard its own state writes; the hook cannot undo arbitrary application side effects. `useInfiniteScroll` remains the simpler intersection-sentinel API.

## Chat: follow new messages without fighting readers

[Complete chat example](examples/useStickToBottom.tsx). Attach scrollRef to the overflow container and contentRef to its content wrapper. Resize/mutation observation schedules a frame only while following is active. Scroll-up, upward wheel intent, and upward navigation keys pause following; explicit scrollToBottom resumes it. Reaching the bottom through scrolling also resumes following. Active text selections inside the container suppress automatic movement.

Native smooth scrolling respects reduced motion. `stopScroll()` cancels pending work and following; it does not claim to cancel a native smooth scroll already handed to the browser. No spring animation, inverse/column-reverse layout, global mouse listeners, or continuously running frame loop is implemented. `initial` is captured once. `enabled: false` removes listeners and queued work; explicit actions do not scroll while disabled.

## Timers: sampling cadence is not elapsed time

- [Stopwatch](examples/useStopwatch.tsx): start/resume, precise pause, reset, and display components.
- [Duration timer](examples/useTimer.tsx): pause/resume/restart and one onExpire call per running generation.

Both use timestamp differences rather than counting interval callbacks. Resolution controls UI sampling; it is clamped to at least one millisecond. Pausing/removing a timer removes its interval. The first render is deterministic for server rendering; auto-start happens after mount. initialElapsed/autoStart configure the initial lifecycle. initialDuration does not restart a running timer on prop changes; use restart explicitly. Display components use whole seconds plus a separate millisecond component. Clock changes can affect elapsed time; these are UI timers, not an authoritative billing clock. Use useCountdown for an absolute server deadline and useNow for wall time.

## Stable refs and selective stores

useLatest and useEventCallback already refresh after commit. A suspended/abandoned render is tested to leave committed values untouched. We did not add useCallbackRef as an alias for the same behavior.

useComposedRef supports React 18 null-detachment and React 19 cleanup-returning refs. Keep the number of ref slots fixed and pass null for an unused slot. Attach the returned callback to an element so React can detach old constituent refs when identities change. Callback refs are deduplicated per attachment.

useEventListener now accepts object refs and SVG elements. It resolves refs after each commit and replaces subscriptions only when the actual target or listener options change. A silent ref.current mutation outside a React commit is not observable; use a callback ref/state target when replacements must be immediate. once/signal retain native browser behavior.

Inspired by react-tracked, the store work focuses on avoiding unrelated renders: useStoreSelector now preserves equal selected objects across parent renders even with inline selectors. It keeps explicit selection/equality rather than importing proxy tracking or attempting a new provider system. Snapshots must remain cached/immutable, and selector/equality functions should be pure.

## Verification

The checks cover native observer sharing, latest-entry selection, logical-box axes, cleanup, hydration, memory fallback, stale decoding, scheduler edge behavior, maximum waiting, abandoned renders, inline-selector identity, range cancellation/deduplication, and timer generations. Every catalog hook has browser Strict Mode and no-browser SSR smoke cases. All complete examples and public consumer type tests are compiled. The reusable isolated-consumer check is also configured in CI for React 19 types, keeping them separate from React 18 build types. Gzip checks cover independent feature groups, the existing tiny-hook entries, and a consumer intentionally importing the full catalog; React is externalized. These are bundle/work-count checks, not a claim of browser timing benchmarks.

## Expanded study: 25 additional packages

Together with the first table, this covers all **43 package references** supplied in the request. Versions below identify the published artifacts studied, not a compatibility promise or a claim that their APIs are interchangeable. We inspected their shipped source, declarations, examples, metadata, and license information where supplied. Implementations here are independent; no upstream runtime package was installed. `use-timer` reports ISC; the other packages in this second table report MIT.

| Reference | Version / license | Our API | Decision and implementation |
| --- | --- | --- | --- |
| [react-use-set](https://github.com/SevenOutman/react-use-set) | 1.0.0 / MIT | useSet | Added bulk removal, replacement, mount-baseline reset, and size. No-op replacements preserve the snapshot. |
| [react-beforeunload](https://github.com/aliebuck/react-beforeunload) | 2.7.0 / MIT | useUnsavedChanges | Retained native beforeunload prompting only while dirty. Custom browser prompt text and SPA navigation blocking are separate concerns. |
| [use-onclickoutside](https://github.com/Andarist/use-onclickoutside) | 0.4.1 / MIT | useClickOutside | Added multiple inside refs for portals, ignored refs, optional scrollbar exclusion, and post-commit owner-document reconciliation. |
| [react-cool-onclickoutside](https://github.com/wellyshen/react-cool-onclickoutside) | 1.7.0 / MIT | useClickOutside | Applied the multi-surface and ignore-ref capabilities above. Shadow DOM event paths remain supported; no second click-outside alias. |
| [@react-hook/merged-ref](https://github.com/jaredLunde/react-hook) | 1.3.2 / MIT | useComposedRef | Retained deduplication and React 19 callback-ref cleanup support, beyond simple merged assignment. |
| [use-previous](https://github.com/Andarist/use-previous) | 1.2.0 / MIT | usePrevious | Retained previous committed value semantics; added regression coverage alongside useConstant. |
| [use-elapsed-time](https://github.com/vydimitrov/use-elapsed-time) | 3.0.4 / MIT | useElapsedTime (new) | Added a pausable active-frame clock, millisecond durations, display update throttling, once-per-run completion, and zero idle frames when paused/completed. |
| [create-context-state](https://github.com/remirror/remirror) | 2.0.3 / MIT | createContextState (new utility) | Added Provider-owned stores with explicit selectors, equality comparisons, stable setters, and server snapshots; no global shared mutable state. |
| [use-error-boundary](https://github.com/JoschuaSchneider/use-error-boundary) | 2.0.6 / MIT | useErrorBoundary (new) | Added a stable descendant boundary, functional fallback, reset, reporting, and explicit showBoundary for caught async/event errors. |
| [@use-it/event-listener](https://github.com/donavon/use-event-listener) | 0.1.7 / MIT | useEventListener | Retained stable commit-refreshed listeners, object/ref targets, target replacement reconciliation, and cleanup. |
| [use-clipboard-copy](https://github.com/wsmd/use-clipboard-copy) | 0.2.0 / MIT | useClipboard | Retained explicit async copy, error state, temporary feedback, and stale-result cleanup. Permission failures do not produce fake success or invoke deprecated execCommand. |
| [use-count-up](https://github.com/vydimitrov/use-count-up) | 3.0.1 / MIT | useCountUp (new) | Added numeric interpolation, custom easing, descending ranges, exact endpoints, pause/reset/resume. Formatting composes with Intl; duration uses milliseconds. |
| [use-local-storage](https://www.npmjs.com/package/use-local-storage) | 3.0.0 / MIT | useLocalStorage | Retained serializers, hydration values, optional memory fallback, and same-tab/cross-tab synchronization from the first study. |
| [use-react-screenshot](https://github.com/vre2h/use-react-screenshot) | 4.0.0 / MIT | useScreenshot (new) | Added renderer injection, abort signals, capture/error state and stale-result protection without bundling html2canvas. Canvas demo runs without another package. |
| [react-container-query](https://github.com/react-container-query/react-container-query) | 0.13.0 / MIT | useContainerQuery (new) | Added named inclusive size rules on the shared frame-batched observer. Pixel changes within the same breakpoint cause no React state updates. |
| [@use-it/interval](https://github.com/donavon/use-interval) | 1.0.0 / MIT | useInterval | Retained latest committed callback, null-delay suspension, Strict Mode cleanup, and no render-driven resubscription. |
| [react-hook-inview](https://github.com/bitmap/react-hook-inview) | 4.5.1 / MIT | useIntersectionObserver | Retained callback-ref target lifecycle and native entry information. It remains a per-target observer; no unsupported claim of shared pooling. |
| [@nkzw/create-context-hook](https://github.com/nkzw-tech/create-context-hook) | 1.1.0 / MIT | createContextState (new utility) | Consolidated context creation into one selector-based factory. State hooks must run below their own Provider; the factory belongs at module scope. |
| [react-use-clipboard](https://github.com/danoc/react-use-clipboard) | 1.0.9 / MIT | useClipboard | Retained one copy API instead of a text-bound alias; callers pass the latest string to copy and render accessible feedback. |
| [react-use-cookie](https://github.com/tylerwolff/useCookie) | 1.6.1 / MIT | useCookie (new) | Added encoded cookies, scope-aware deletion, attribute validation, same-document notifications and event-based refresh. HttpOnly is intentionally unavailable to JavaScript. |
| [react-useportal](https://github.com/alex-cory/react-useportal) | 1.0.19 / MIT | usePortal (new) | Added an owned, effect-created host. Applications call react-dom createPortal; host ownership and cleanup work with alternate documents. |
| [use-persisted-state](https://github.com/donavon/use-persisted-state) | 0.3.3 / MIT | useLocalStorage / useSessionStorage | Existing key-based subscriptions already share changes. A small application wrapper can fix a key without adding a global storage factory or runtime dependency. |
| [use-constant](https://github.com/Andarist/use-constant) | 2.0.0 / MIT | useConstant | Retained lazy value wrappers that correctly cache undefined; added regression coverage. Initializers must remain pure under Strict Mode. |
| [use-timer](https://github.com/thibaultboursier/use-timer) | 2.0.1 / ISC | useTimer / useStopwatch / useElapsedTime | Retained controlled timers for wall-clock durations; added active frame timing for animation. No duplicate seconds-based API. |
| [react-autosave](https://github.com/jollyjerr/react-autosave) | 0.5.0 / MIT | useAutosave | Retained debounced, serialized, abortable writes with queued edits and explicit flush/retry. Async persistence is not silently started on unmount. |

Every new hook and the context factory has a complete, type-checked example and a commented signature in [API.md](API.md). The study favors capabilities with a clear job over duplicate names. Retained APIs above are deliberate scope decisions, not claims to reproduce every feature of each reference.

### Three different clocks

`useTimer` and `useStopwatch` use wall-clock time for timers. `useElapsedTime` measures active animation-frame deltas; paused time is excluded and inactive browser tabs follow browser frame scheduling. `useCountUp` interpolates numeric values on that active clock. Durations throughout these new APIs are **milliseconds**, unlike the seconds-based interfaces in some references. Set `playing: false` for an external suspension; local `pause()` and `resume()` are independent controls. After completion, call `reset()` and `resume()` to replay. Inputs such as endpoints and duration are live settings and do not implicitly restart the run.

### Context without the rerender conga

Create `createContextState` at module scope, then wrap consumers in its `Provider`. Each Provider owns a separate store. `useSelector` compares the selected output; unrelated fields can change without rerendering that subscriber. `useUpdate` reads only the stable setter. Immutable snapshots are required, and changing the Provider's `initialValue` does not reset an already-mounted store. Remount with a new React key when switching datasets should reset state.

### Honest browser boundaries

Screenshot capture requires an injected renderer; it is not an HTML rendering engine. The library passes an AbortSignal and rejects obsolete results even if a renderer ignores cancellation. The working canvas example needs no image dependency. Portal hosts are null during SSR and initial hydration, are owned by the hook, and never adopt or remove preexisting DOM nodes. Portals alone do not implement accessible dialogs.

Cookie values are initially undefined for consistent server hydration. Same-document writes notify subscribers. Cookie Store events, focus, visibility and explicit refresh cover external changes without polling; unsupported browsers cannot observe every arbitrary external write immediately. JavaScript cannot read or create HttpOnly cookies. A successful cookie setter means the requested readable value was observed, not that storage durability or server acceptance is guaranteed.

Error boundaries handle descendant React render failures. For async or event failures, catch the error and call `showBoundary(error)`. They do not catch server rendering errors or errors thrown by their own fallback. Render one returned boundary per hook instance.
