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

@stealthscale/hooks

v0.1.0

Published

The React hooks a component uses to read the page it draws into: how the page is read, what it measures, and what a value was on the previous render.

Readme

@stealthscale/hooks

@stealthscale/hooks publishes the React hooks a component uses to read the page it draws into. Some measure the document. Some keep a value across renders without causing another render. One writes a message to a screen reader. Every hook depends on React and on the document and on nothing else, so a package that draws no component installs only React to use them.

Install

pnpm add @stealthscale/hooks

The package peers on react and nothing else.

useConst

Builds a value on the first render and returns that same value on every render after it.

Use it when the identity of a value matters and building it costs something: a collator, an observer, an object used as a map key. useMemo is a cache the runtime may drop and rebuild, so a caller that compares by identity cannot use it. The factory runs on the first render only. To hold a value that is itself a function, return it from the factory.

const collator = useConst(() => new Intl.Collator(locale, { sensitivity: "base" }));

const sorted = useMemo(() => [...names].sort(collator.compare), [collator, names]);

useLiveRef

Points a ref at the value this render was given, and returns the ref.

Use it when a callback or an effect has to read the current value without listing it as a dependency. A resize handler that listed the width it compares against would detach and reattach its observer on every render. The ref is written during the render rather than in an effect, so code that runs before the effects reads this render's value.

const latest = useLiveRef(onResize);

useEffect(() => {
  const observer = new ResizeObserver(() => latest.current(element));

  observer.observe(element);

  return () => observer.disconnect();
}, [element, latest]);

useCallbackRef

Returns a stable function that calls whichever callback the latest render passed.

Use it when you are about to put a handler in a dependency array. A handler written inline is a new closure on every render, so the effect re-runs on every render. List the function this hook returns instead, and the effect re-runs only when its other dependencies change. To make the returned function change identity, pass those dependencies as the second argument.

export function useDismiss(onDismiss?: () => void) {
  const dismiss = useCallbackRef(onDismiss);

  useEffect(() => {
    const escape = (event: KeyboardEvent) => {
      if (event.key === "Escape") dismiss();
    };

    document.addEventListener("keydown", escape);

    return () => document.removeEventListener("keydown", escape);
  }, [dismiss]);
}

useSafeLayoutEffect

Measures and writes before the browser paints, and does nothing on a server.

Use it instead of useLayoutEffect in any component that renders on a server. A layout effect runs before the paint, so a component that positions itself is never drawn in the wrong place first. A server has nothing to measure and nothing to paint, and React logs a warning for useLayoutEffect there rather than skipping the call. This hook falls back to useEffect on a server, and React runs no effects there.

useSafeLayoutEffect(() => {
  const { height } = content.current.getBoundingClientRect();

  panel.current.style.setProperty("--panel-height", `${String(height)}px`);
}, []);

useControllableState

Returns the value and a setter, and takes the value from the caller when the caller passes one.

Use it for any piece of state a caller might want to own: an open flag, a selected value, a search term. One component then serves a caller that sets the value and a caller that leaves the component to hold it. Passing value is what makes the state controlled, so a caller that passes undefined leaves the component in control. The hook re-reads value on every render, so a caller that starts setting it partway through takes over from that render.

export function Disclosure(props: DisclosureProps) {
  const [open, setOpen] = useControllableState({
    defaultValue: props.defaultOpen ?? false,
    onChange: props.onOpenChange,
    value: props.open,
  });

  return <button onClick={() => setOpen((was) => !was)}>{open ? "Hide" : "Show"}</button>;
}

onChange is called whenever the value changes, controlled or not. A set to the value already held is dropped, so a caller listening for changes receives only changes.

useMediaQuery

Reads whether each query matches, and re-reads them whenever any of them changes.

Use it when a decision depends on the page rather than on a prop. CSS cannot express a decision that changes what is rendered rather than how it looks. Every query is read from one window, and the results come back in the order the queries were written, so a caller can index the result by its own query. A change re-reads every query, because one match changing can change another.

const [wide, dark] = useMediaQuery(["(min-width: 60rem)", "(prefers-color-scheme: dark)"]);

return wide ? <Sidebar /> : <Drawer />;

The first render returns the fallback rather than reading the window, so a server's markup and the client's first paint agree. Pass ssr: false to read the window on that first render. Pass fallback to set what each query returns until the window has been read.

useCoarsePointer

Reads whether the reader's main pointer is a finger rather than a mouse or a pen.

Use it where an interaction assumes a fine pointer. A finger cannot rest on an element, so anything that appears on hover alone never appears. A finger cannot aim at a hairline, so a small target has to grow.

const touch = useCoarsePointer();

return (
  <Row data-density={touch ? "comfortable" : "compact"}>
    {touch ? <MoreButton /> : <HoverActions />}
  </Row>
);

useIsOverflowing

Watches an element and returns whether the content inside it is cut off.

Use it when text that does not fit is what decides whether to draw something else. A tooltip carrying the full label, a read-more control and a fade at the edge all need that result. CSS truncates the text and reports nothing.

The element is measured again on four occasions:

  • It changes size.
  • Its content changes.
  • Its parent changes size.
  • The fonts finish loading.
const ref = useRef<HTMLSpanElement>(null);
const { overflows } = useIsOverflowing(ref);

return (
  <Tooltip content={overflows ? children : undefined}>
    <span ref={ref}>{children}</span>
  </Tooltip>
);

The returned object keeps one identity while the result stands, so the component renders again only when the element crosses between fitting and not.

useStickyOffsets

Sets a custom property on each sticky band holding the height of the bands before it, and one on the column holding the height of all of them.

Use it when more than one band sticks in the same scroll container. A page header above a toolbar above a table head is three of them. The second band has to sit below the first, and position: sticky gives it no way to read how tall the first is.

useStickyOffsets(column, stuck, {
  bands: "[data-band]",
  offset: "--band-offset",
  total: "--stuck-height",
});
[data-band] {
  position: sticky;
  top: var(--band-offset, 0);
}

A band rendered later is measured when the column next changes size rather than on a size change of its own.

useMatrixCrosshair

Returns the ref to put on a grid and the two handlers that mark the row and the column under the pointer.

Use it in a grid wide enough that "row 14, column 9" is a counting exercise. Column headers in such a grid carry short names, because rotated text is unreadable and most magnifiers cannot show it. This hook turns a short name back into a row somebody can read. The marks are written through the DOM rather than through state, because a matrix of a few hundred items either way is tens of thousands of cells.

Mark the cells and the headers of a row with data-row and those of a column with data-column. The hook sets data-lit on both sets while the pointer is over them.

const { clear, ref, track } = useMatrixCrosshair<HTMLTableElement>();

return (
  <table ref={ref} onPointerMove={track} onPointerLeave={clear}>
    <td data-row={rowId} data-column={columnId} />
  </table>
);
td[data-lit] {
  background: var(--colors-bg-muted);
}

An element may carry both attributes, which is what lets a matrix of one set against itself mark the row a hovered column stands for.

useAnnounce

Returns a function that announces a message to a screen reader.

Use it when something changes that the page shows without words: "5 results", "copied", "row removed". The change is plain on screen and silent to anything reading the page aloud. polite waits for a gap and suits nearly everything. assertive interrupts whatever is being read mid-word, which suits an error that invalidates what somebody is doing and nothing else.

const announce = useAnnounce();

const copy = async () => {
  await navigator.clipboard.writeText(value);
  announce("Copied");
};

const onFail = (error: Error) => {
  announce(`Could not save. ${error.message}`, "assertive");
};

Messages queued in one frame are joined into a single utterance, because a live region announces one thing per change. One component announcing twice is correcting itself, so its later message replaces its earlier one. Where two components announce, both messages are read. The same message twice is announced twice, because "copied" pressed twice is two events somebody wants confirmed.

speakable

Joins a frame's messages into the one string the region announces.

useAnnounce calls this itself. Call it directly only when you write into a live region of your own and want the same joining rule. A full stop is added only where the message before it ends in none, and a message repeated in the same frame appears once.

speakable(["Saved", "3 rows selected", "Saved"]);

That returns Saved. 3 rows selected.

createRequiredContext

Makes a context a reader has to be inside, and the two hooks that read it.

Use it for a component drawn in parts, where every part needs something the root holds. React returns the default value for a missing provider, so a part drawn outside its root draws wrongly, reports nothing, and fails somewhere else. This throws where the part was written instead, and names the component so the message says which root is missing.

const [ApiProvider, useCollapsible] = createRequiredContext<CollapsibleApi>("Collapsible");

function Root({ children }: RootProps): ReactElement {
  const api = collapsible.connect(useMachine(collapsible.machine, { id: useId() }), normalizeProps);

  return <ApiProvider value={api}>{children}</ApiProvider>;
}

function Trigger(props: TriggerProps): ReactElement {
  const api = useCollapsible();

  return <Styled {...mergeProps(api.getTriggerProps(), props)} />;
}

A trigger drawn with no root above it throws A part of Collapsible was drawn outside the root that holds it together. A reader under two providers gets the value of the nearer one. Each call makes a context of its own, so two components never read each other's.

The third member of the tuple reads the same context and returns undefined where no provider stands above it. A root that nests inside another of its own kind reads that hook to find out whether it is the outermost.

Types

| Type | Declaration | What it describes | | --------------------------- | ------------------------- | --------------------------------------------------------------------------- | | Overflow | interface | Whether content is cut off across, down, or on either axis | | MatrixCrosshair | interface | The ref and the two pointer handlers a grid attaches | | UseControllableStateProps | interface | The value, the default, and the callback run on every change | | UseMediaQueryOptions | interface | What each query returns before the window is read, and which window to read | | UseStickyOffsetsOptions | interface | Which bands stick, and which custom properties carry their offsets | | AnnouncePoliteness | "assertive" \| "polite" | How much a message is allowed to interrupt | | ProvidedProps | interface | The value a provider carries and the tree that reads it |

Licence

MIT. See LICENSE.