@forthtilliath/react-kit
v0.5.0
Published
Small React building blocks — hooks and headless control-flow components — with no styling opinions. The web-React counterpart to `@forthtilliath/react-native-kit`. Formerly published as two separate packages, `@forthtilliath/react-hooks` and `@forthtilli
Readme
@forthtilliath/react-kit
Small React building blocks — hooks and headless control-flow components —
with no styling opinions. The web-React counterpart to
@forthtilliath/react-native-kit. Formerly published as two separate
packages, @forthtilliath/react-hooks and @forthtilliath/react-ui (both
now deprecated in favor of this one).
Install
npm install @forthtilliath/react-kitOr, from within this monorepo, as a workspace dependency:
{
"dependencies": {
"@forthtilliath/react-kit": "workspace:*"
}
}Usage
Each hook/component is its own module — import the file you need directly:
import {
SPECIAL_KEYS,
useKeyListener,
} from "@forthtilliath/react-kit/useKeyListener";
import { useToggleState } from "@forthtilliath/react-kit/useToggleState";
import { usePersistentState } from "@forthtilliath/react-kit/usePersistentState";
import { useDebounce } from "@forthtilliath/react-kit/useDebounce";
import { useThrottle } from "@forthtilliath/react-kit/useThrottle";
import { useMediaQuery } from "@forthtilliath/react-kit/useMediaQuery";
import { useClickOutside } from "@forthtilliath/react-kit/useClickOutside";
import { useOnlineStatus } from "@forthtilliath/react-kit/useOnlineStatus";
import { useIntersectionObserver } from "@forthtilliath/react-kit/useIntersectionObserver";
import { useCopyToClipboard } from "@forthtilliath/react-kit/useCopyToClipboard";
import { useControllableState } from "@forthtilliath/react-kit/useControllableState";
import { useHorizontalScroll } from "@forthtilliath/react-kit/useHorizontalScroll";
import { Repeat, type RepeatProps } from "@forthtilliath/react-kit/repeat";
import { Show, type ShowProps } from "@forthtilliath/react-kit/show";
import {
SlotOrCallback,
type SlotOrCallbackProps,
} from "@forthtilliath/react-kit/slot-or-callback";useKeyListener(config, onKeyDown)
Attaches a window keydown listener and calls onKeyDown(event) when the
event matches config:
key—KeyboardEvent.keyto match; any key when omitted.ctrl,alt,meta— must match exactly (falseby default):{ key: "s" }doesn't fire on Ctrl+S, so it never clashes with a browser/OS shortcut.shift— only checked when specified, since Shift is often needed to type the key itself ("?", uppercase letters…).
onKeyDown can be an inline function: the listener is only re-attached when
config's values change. Also exports a SPECIAL_KEYS constant (ENTER,
SPACE, ESCAPE, BACKSPACE, TAB) for the key field:
useKeyListener({ key: SPECIAL_KEYS.ESCAPE }, () => setOpen(false));
useKeyListener({ key: "s", ctrl: true }, (event) => {
event.preventDefault(); // keep the browser's "Save page" dialog closed
save();
});useToggleState(defaultValue?)
Like useState for a boolean, plus a ready-made toggler:
const [isOpen, setIsOpen, toggleOpen] = useToggleState(false);Returns [value, setValue, toggle] as const.
useDebounce(value, delayMs)
Returns a debounced copy of value, updated only once it has stopped
changing for delayMs — the classic guard against firing a request/filter
on every keystroke:
const debouncedQuery = useDebounce(query, 300);useThrottle(value, limitMs)
Returns a throttled copy of value, updated at most once per limitMs
(immediately on the first change, then on a trailing edge so the final value
is never dropped). Suited to high-frequency sources (scroll, resize,
pointer move):
const throttledScrollY = useThrottle(scrollY, 200);useMediaQuery(query)
Tracks whether a CSS media query currently matches. SSR-safe — returns
false until mounted, then stays in sync:
const isDesktop = useMediaQuery("(min-width: 1024px)");useClickOutside(onClickOutside)
Returns a ref; calls onClickOutside on a pointer event outside that ref's
element — the pattern behind closing a dropdown/popover/modal on an outside
click:
const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));useOnlineStatus()
Tracks navigator.onLine, updated live via the online/offline events:
const isOnline = useOnlineStatus();useIntersectionObserver(options?)
Returns [ref, isIntersecting] for the returned ref's element — the
building block behind lazy-loading, infinite scroll and scroll-triggered
animations. Pass once: true to stop observing after the first time it
becomes visible. ref is a callback ref: an element rendered conditionally,
after the first render, is observed as soon as it mounts:
const [ref, isVisible] = useIntersectionObserver<HTMLImageElement>({
once: true,
});useCopyToClipboard(resetDelayMs?)
Returns [copiedText, copy]. copy(text) writes to the clipboard and
resolves to whether it succeeded; copiedText holds the last copied value
and auto-clears after resetDelayMs (default 2000) — handy for a
"Copied!" button state:
const [copiedText, copy] = useCopyToClipboard();useControllableState({ value?, defaultValue?, onChange? })
Backs a headless component that must support both controlled (value +
onChange, parent owns the state) and uncontrolled (defaultValue,
component owns the state) usage through a single code path:
const [pressed, setPressed] = useControllableState({
value,
defaultValue,
onChange,
});useHorizontalScroll<TInner>({ step?, keyboard? })
Tracks whether a horizontally scrollable container can scroll further left
or right — for edge shadows or arrow buttons — kept in sync via a
ResizeObserver on the container (scrollRef) and its content
(innerRef). scrollByStep(-1 | 1) smooth-scrolls by step px (default
240); the left/right arrow keys do the same unless focus is in a form field
(keyboard: false to opt out). Call updateScrollState from onScroll.
scrollRef/innerRef are callback refs, so a container rendered
conditionally is tracked too:
const {
scrollRef,
innerRef,
canScrollLeft,
canScrollRight,
updateScrollState,
scrollByStep,
} = useHorizontalScroll<HTMLTableElement>();
<div ref={scrollRef} onScroll={updateScrollState} className="overflow-x-auto">
<table ref={innerRef}>…</table>
</div>;Show<T>
Conditionally renders children, with an optional fallback. When when is
a value (not just a boolean), children can be a render function that
receives the narrowed, non-nullish value:
<Show when={user} fallback={<Spinner />}>
{(u) => <p>Hello {u.name}</p>}
</Show>Repeat
Renders children count times. children can be a static node or a
render function receiving the current index:
<Repeat count={5}>{(i) => <Star key={i} />}</Repeat>SlotOrCallback
Accepts children as either a plain React node or a function-as-children
render prop, and normalizes both into a rendered node — used internally by
components that want to support both patterns without duplicating logic.
A render function is called with args, which is required as soon as the
function declares parameters:
<SlotOrCallback args={[user]}>{(u) => <p>Hello {u.name}</p>}</SlotOrCallback>Scripts
pnpm run dev # tsc --watch -> dist/
pnpm run build # tsc -> dist/
pnpm run check-types # tsc --noEmit
pnpm run lint # eslint
pnpm run test # vitest run
pnpm run test:watch # vitestEverything is built to dist/ (see the exports field in package.json),
so run pnpm run build (or dev) after source changes for consumers to see
them.
