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

@onoxm/react-hooks

v0.6.7

Published

React hooks library by React + Typescript

Readme

@onoxm/react-hooks

A collection of React hooks for TypeScript — events, DOM measurement, keyboard shortcuts, theming and countdowns.

All hooks are also re-exported from the ono-react-element entry point.

Installation

npm install @onoxm/react-hooks
# or
pnpm add @onoxm/react-hooks
# or
yarn add @onoxm/react-hooks

Peer dependency: react (>= 19). @onoxm/utils is a regular dependency and is installed automatically.

Hooks overview

| Hook | Purpose | | ----------------------------------------- | ---------------------------------------------------------------- | | useEventListener | Attach event listeners with a stable handler reference | | useClickOutSide | Detect clicks outside one or more elements | | useCountdown | Countdown timer with start / reset | | useDefer | Defer expensive rendering by animation frames | | useGetElementSize | Observe an element's size (or the window's) | | useKeyPress | Keyboard shortcuts, including combinations | | useMouseClick | Track the last click position | | useTheme | Resolve light / dark / system into a boolean | | useThemePro | Theme switching with a circular reveal animation | | useWindowSize | Track the window size — ⚠️ deprecated, use useGetElementSize |

API

useEventListener

Attaches an event listener to a target and cleans it up automatically.

useEventListener(event: string | string[], handler: (e: Event) => void, options?: {
  target?: EventTarget | RefObject<EventTarget | null> | null | undefined
  listenerOptions?: boolean | AddEventListenerOptions
}): void
import { useRef } from 'react'
import { useEventListener } from '@onoxm/react-hooks'

function Box() {
  const ref = useRef<HTMLDivElement>(null)

  // Multiple events at once; target defaults to window
  useEventListener(['pointerdown', 'pointerup'], e => console.log(e.type), {
    target: ref
  })

  return <div ref={ref} />
}

Notes:

  • target resolution: null / undefined ⇒ window; a RefObject ⇒ its .current, falling back to window while it is still null; otherwise the EventTarget itself.
  • The handler is kept in a ref and always called with the latest closure, so you do not need useCallback — a new inline function on every render does not rebind the listener.
  • An array of events is normalized so that a new array with the same contents does not rebind either.
  • Because binding happens inside useEffect, this hook is safe under SSR.

useClickOutSide

Calls the handler when a click happens outside the given element(s).

useClickOutSide(outsideElement, handler: (e: MouseEvent) => void, options?: {
  event?: 'click' | 'contextmenu' | 'mousedown' | 'mouseup' | 'mousemove' | 'touchstart' | 'touchend'
          | (…)[]
}): void
function Dropdown({ onClose }: { onClose: () => void }) {
  const ref = useRef<HTMLDivElement>(null)
  useClickOutSide(ref, onClose) // default event: 'click'
  useClickOutSide([ref, otherRef], onClose, {
    // several elements
    event: ['mousedown', 'touchstart']
  })
  return <div ref={ref}>…</div>
}

Notes:

  • outsideElement accepts a single element, a ref, or an array of either, and may be null.
  • The listener is attached to window; the handler receives the original MouseEvent.
  • Duplicate event types throw — { event: ['click', 'click'] } raises an error instead of silently listening twice.
  • If the referenced elements are not mounted yet, no element is found, so every click counts as "outside" and the handler fires immediately.

useCountdown

useCountdown(initialTime?: number /* = 60 */, onFinish?: () => void)
  : readonly [remainingTime: number, startCountdown: () => void, resetCountdown: () => void]
function Resend() {
  const [left, start, reset] = useCountdown(60, () => console.log('done'))

  return (
    <button disabled={left !== 60} onClick={start}>
      {left === 60 ? 'Send code' : `Resend in ${left}s`}
    </button>
  )
}

Notes:

  • The countdown starts paused; call startCountdown() to begin. It ticks once per second.
  • When it reaches 0, it calls onFinish and automatically resets remainingTime back to initialTime, then stops.
  • ⚠️ resetCountdown() also invokes onFinish — it stops the timer, restores initialTime, and calls the callback.
  • onFinish is read through a ref, so you can pass a new inline function without restarting the timer.

useDefer

Defers expensive rendering by counting animation frames.

useDefer(maxCount?: number /* = 100 */): (n: number) => boolean
function List({ items }: { items: Item[] }) {
  const defer = useDefer()
  // Render the heavy tree only after a few frames have elapsed
  return defer(2) ? <Heavy items={items} /> : <Skeleton />
}

Notes:

  • Returns a predicate: defer(n) is true once more than n frames have elapsed since mount.
  • The internal frame counter stops incrementing at maxCount, so predicates with n >= maxCount are never satisfied — keep n below maxCount.

useGetElementSize

Observes the size of an element (or the window) via ResizeObserver.

useGetElementSize(target?: ElementSizeTarget): { w: number; h: number }
function Card() {
  const ref = useRef<HTMLDivElement>(null)
  const { w, h } = useGetElementSize(ref)
  return (
    <div ref={ref}>
      {w} × {h}
    </div>
  )
}

target may be an HTMLElement, a RefObject<HTMLElement | null>, a function returning either (useful for lazy lookups), or null.

Notes:

  • No target at all ⇒ the hook reports the window size and follows window resizes.
  • A target was passed but the element is not mounted yet ⇒ the hook keeps its current size and waits, and never falls back to the window size. (The previous implementation did, which made the first frame render at full viewport size.)
  • Sizes come from borderBoxSize when available, falling back to contentRect.
  • If the size has not changed, the previous state object is returned, avoiding a re-render.
  • One ResizeObserver is created per component instance and reused across renders; the previous element is unobserved when target changes.
  • Under SSR, or where ResizeObserver does not exist, it returns { w: 0, h: 0 } instead of throwing.

useKeyPress

Listens for a key or a key combination.

useKeyPress(
  keyFilter: KeyType | KeyType[],
  eventHandler: (event: KeyboardEvent, key: KeyType) => void,
  options?: {
    events?: 'keydown' | 'keyup' | ('keydown' | 'keyup')[]
    target?: EventTarget | RefObject<EventTarget | null> | null
    exactMatch?: boolean
  }
): void
function Search() {
  useKeyPress('k', () => focusSearch(), { exactMatch: true })

  // Combinations: '+'-joined, case-insensitive
  useKeyPress('ctrl+s', e => {
    e.preventDefault()
    save()
  })
  useKeyPress(['shift+alt+d', 'ctrl+shift+d'], () => toggleDevtools())

  // Works on macOS and Windows/Linux alike
  useKeyPress('CommandOrControl+p', openPalette)

  // Several keys at once, listening to keyup instead
  useKeyPress(['Escape', 'Enter'], (e, key) => console.log(key, 'on', e.type), {
    events: 'keyup'
  })

  return <input />
}

Notes:

  • Matching is case-insensitive — filters are lower-cased before comparison, so 'K' and 'k' are the same.
  • Combinations use +: two parts (ctrl+s) require one modifier, three parts (ctrl+shift+s) require the combination. Supported modifiers: Ctrl, Shift, Alt, Meta, and CommandOrControl (Ctrl on Windows/Linux, Cmd on macOS).
  • The handler receives the matched filter entry as written by you as its second argument, e.g. 'ctrl+s'.
  • options.events defaults to 'keydown'.
  • options.exactMatch (default false): for single-key filters, a modifier held down prevents the match. Set it to true to require the key to match exactly — handy for shortcuts like 'k' that should not fire while typing Ctrl+K.
  • options.target defaults to window.

useMouseClick

Tracks the position of the last click anywhere on the page.

useMouseClick(): {
  clientX: number; clientY: number
  screenX: number; screenY: number
  pageX: number; pageY: number
}
function Cursor() {
  const { clientX, clientY } = useMouseClick()
  return <div style={{ left: clientX, top: clientY }} />
}

Notes:

  • All six coordinates start at 0 and update on every click (attached to window).

useTheme

Resolves a theme mode into the actual light/dark state and notifies you when it changes.

useTheme(props: {
  theme: 'light' | 'dark' | 'system'
  onDark: () => void
  onLight: () => void
  useThreeMode?: boolean
}): { cycleTheme: () => ThemeMode; isDark: boolean }
function ThemeWatcher({ theme }: { theme: ThemeMode }) {
  const { isDark, cycleTheme } = useTheme({
    theme,
    onDark: () => console.log('dark applied'),
    onLight: () => console.log('light applied')
  })
  return (
    <button onClick={() => setTheme(cycleTheme())}>Next: {cycleTheme()}</button>
  )
}

Notes:

  • isDark is the resolved value — for 'system' it follows the operating system, so judge lightness/darkness by isDark, not by theme.
  • onDark / onLight fire on every application: on mount, whenever theme changes, and — while the mode is 'system' — whenever the OS preference changes.
  • cycleTheme() only returns the next mode; it does not switch. The order is system → light → dark → system when useThreeMode is true (the default), and light ↔ dark when it is false.
  • System changes are only tracked while theme === 'system'.
  • Callbacks are read through refs, so new inline functions do not cause repeated work.
  • SSR-safe: the initial isDark is derived from theme alone ('system' counts as light) to avoid a hydration mismatch, and is corrected immediately in a layout effect.

useThemePro

Theme switching with a circular-reveal animation, built on useTheme.

useThemePro(options?: {
  initTheme?: ThemeMode          // default 'light'; applies on first render only
  themeRules?: (isDark: boolean) => void
  useThreeMode?: boolean         // default true
  duration?: number              // default 300 (ms)
  expandOn?: 'dark' | 'light'    // which theme the circular reveal plays on; default 'dark'
}): {
  theme: ThemeMode
  isDark: boolean
  changeTheme: (o: { targetTheme: ThemeMode; element?: HTMLElement; onChange?: () => void }) => void
  nextTheme: (o?: { element?: HTMLElement; onChange?: () => void }) => void
}
function Toggle() {
  const { isDark, nextTheme } = useThemePro({
    initTheme: 'system',
    themeRules: isDark => {
      document.documentElement.classList.toggle('dark', isDark)
      // Don't forget this line — see the note below
      document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'
    }
  })

  return (
    <button onClick={e => nextTheme({ element: e.currentTarget })}>
      {isDark ? '🌙' : '☀️'}
    </button>
  )
}

Notes:

  • themeRules is the single outlet for applying a theme. The default adds/removes a dark class on <html>; pass your own to write to a data attribute, CSS variables, a store, analytics, and so on.
  • ⚠️ themeRules is called on every application, not only on changes: on mount, on every changeTheme / nextTheme, and on system changes while in 'system' mode. It must be idempotent — set the value from isDark directly; do not write toggle logic, which would invert on the extra call.
  • ⚠️ Set document.documentElement.style.colorScheme yourself. Without it, native scrollbars, <select> dropdowns, date pickers and input autofill stay light in dark mode.
  • element is the origin of the circular reveal. When omitted, the previous element is reused so that later switches keep animating; only user-initiated switches animate.
  • System-driven changes are applied silently (no animation) — the stored element belongs to a button that had nothing to do with the system change.
  • changeTheme / nextTheme are no-ops when the target mode equals the current one, and onChange is not called in that case.

useWindowSize

⚠️ Deprecated — will be removed in 0.7.x. Use useGetElementSize() with no argument instead: it is a strict superset (same window tracking, plus element observation, and it skips the re-render when the size did not change). useWindowSize is now a thin wrapper over it, kept only for backwards compatibility.

useWindowSize(): { width: number; height: number }
// Old
function Layout() {
  const { width, height } = useWindowSize()
  return (
    <p>
      {width} × {height}
    </p>
  )
}

// New — same behaviour, superset of features
function Layout() {
  const { w, h } = useGetElementSize()
  return (
    <p>
      {w} × {h}
    </p>
  )
}

Notes:

  • The returned object is memoized, so it keeps a stable reference while the size is unchanged — safe to use as an effect / memo dependency.
  • Because it delegates to useGetElementSize, resizes that do not change the size no longer trigger a re-render (the old implementation did).

Exports

// Hooks
useEventListener
useClickOutSide
useCountdown
useDefer
useGetElementSize
useKeyPress
useMouseClick
useTheme
useThemePro
useWindowSize // deprecated, will be removed in 0.7.x

// Types
ElementSize
ElementSizeTarget
KeyType
KeyEvent
ThemeMode
ThemeRules
UseThemeProOptions
UseThemeProReturn

Features

  • 🪝 Ten focused hooks, all tree-shakable
  • 🔥 Full TypeScript support
  • 🧷 Stable handler references — no useCallback boilerplate
  • 🌗 Theme handling with SSR-safe resolution and an optional circular-reveal transition
  • 🧯 SSR-safe: no window / ResizeObserver access outside effects

Author

ono

License

MIT