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

@corndev/ui-hooks

v1.0.0-beta.4

Published

React hooks that every app writes anyway — debounced callbacks, disclosure, clipboard, media queries — with no dependencies beyond React

Readme

@corndev/ui-hooks

The React hooks an app ends up writing for itself when a library does not ship them.

No components, no CSS, no theme — you can take the hooks without taking the design system, and nothing here imports the rest of Corn UI.

npm install @corndev/ui-hooks

React 18 or newer, as a peer dependency.

useDebouncedCallback

Runs a callback once the caller stops calling it for a given number of milliseconds — the hook behind every text field that saves as you type.

import { useDebouncedCallback } from '@corndev/ui-hooks';

function Editor({ doc }) {
  const save = useDebouncedCallback(text => api.put(doc.id, text), 250);

  return (
    <textarea
      defaultValue={doc.text}
      onChange={event => save(event.target.value)}
      onBlur={save.flush}
    />
  );
}

save.flush() runs a pending call immediately, with the arguments it was going to use. save.cancel() discards it.

It cancels on unmount, and that is correctness

A trailing call fires after the user stops typing, which may be after they have navigated away. A delayed write that lands then is writing to a store that no longer describes what is open — which is how a late save ends up stamping one document's contents onto another.

The hook cancels any pending call when the component unmounts, so that cannot happen. Nothing is required of the caller.

The callback that runs is the current one

The hook calls the most recent callback you passed, not the one captured when the timer was scheduled. A body that reads props or state sees the values from the render the timer fired in, rather than the ones from the render that scheduled it — the stale-closure trap that makes a debounced save write values the user has already changed.

Changing the delay

A call already pending keeps the delay it was scheduled with; a new delay applies to the next call. Restarting the clock instead would let a component that re-renders with a changing delay postpone a save indefinitely.

useDisclosure

An open/closed boolean with named controls, for the state behind a modal, a drawer or a collapsible section. onOpen and onClose run only on a real change, and the controls keep one identity, so they are safe in effect dependencies.

import { useDisclosure } from '@corndev/ui-hooks';

const [opened, { open, close, toggle }] = useDisclosure(false, { onClose: reset });

useClipboard

Copies text, and remembers for a moment that it did. copy resolves to whether it worked and never rejects: without a secure (https) page, or when the browser refuses, error says why.

import { useClipboard } from '@corndev/ui-hooks';

const { copy, copied } = useClipboard({ timeout: 2000 });
<button onClick={() => copy(url)}>{copied ? 'Copied' : 'Copy link'}</button>

useMediaQuery

Whether a media query matches, kept current. Safe to render on the server: the first render returns initialValue, and the real answer follows right after, so hydration never mismatches.

import { useMediaQuery } from '@corndev/ui-hooks';

const narrow = useMediaQuery('(max-width: 960px)', { initialValue: false });

useClickOutside

Calls a handler when the pointer goes down outside an element. Spread the result onto the element rather than passing only a ref. It carries a capture-phase handler that makes a portalled child, such as a dropdown rendered into document.body, count as inside, because it is inside in React's tree.

import { useClickOutside } from '@corndev/ui-hooks';

const outside = useClickOutside(() => setOpen(false), { enabled: open });
<div {...outside}>…</div>

ignore: [ref] adds elements outside React that should also count as inside.

useLocalStorage

State kept in localStorage, shared by every component using the key and kept in step with other tabs.

import { useLocalStorage } from '@corndev/ui-hooks';

const [theme, setTheme, { remove, persistent }] = useLocalStorage('theme', 'light');
  • Server-safe: the first render returns the initial value, and the stored value follows.
  • Degrades instead of throwing: in a private window, with blocked site data or past the quota, the value lives in memory for the life of the page, and persistent is false, so the UI can say a setting will not stick.
  • Values are JSON by default; pass serialize and deserialize to change that. A stored value that does not parse is treated as absent.

useHotkeys

Keyboard shortcuts for the page while the component is mounted. mod is ⌘ on Apple platforms and Ctrl elsewhere.

import { useHotkeys } from '@corndev/ui-hooks';

useHotkeys([
  ['mod+k', openSearch],
  ['mod+s', save, { allowInInputs: true }],
  ['escape', close, { allowInInputs: true }],
]);

Shortcuts stay quiet while focus is in a text field, a select or editable content, so binding / does not steal the character from someone typing it. Mark a binding allowInInputs to let it through. A matched shortcut's default action is prevented.

useElementSize

An element's content-box size, kept current, for laying something out against its container rather than the window. 0 × 0 on the server and until the element is attached.

import { useElementSize } from '@corndev/ui-hooks';

const { ref, width } = useElementSize();
<div ref={ref}>{width > 600 ? <Wide /> : <Narrow />}</div>

Server Components

Every hook is its own module with its own 'use client' directive, so importing one from a file that also runs on the server does not pull the others into the browser bundle.

Links

Licence

MIT © Corndog Development LLC