@alphinex/hooks
v1.0.2
Published
Cross-cutting React hooks shared across the platform (useDisclosure, useMediaQuery, ...).
Readme
@alphinex/hooks
Small, cross-cutting React hooks shared across the platform — no UI, no styling, just reusable
stateful logic that @alphinex/ui components and app code both build on.
useDisclosure
Shared open/close boolean state for Dialog, Drawer, Menu, Popover, Tooltip, and anything else with
an open/closed concept. Takes an optional defaultOpen, onOpen, and onClose:
import { useDisclosure } from "@alphinex/hooks";
function DeleteConfirmDialog() {
const { isOpen, open, close, toggle } = useDisclosure({
onClose: () => console.log("dialog closed"),
});
return (
<>
<button onClick={open}>Delete</button>
{isOpen && <Dialog onDismiss={close}>Are you sure?</Dialog>}
</>
);
}useControllableState
Backs every stateful @alphinex/ui component: uncontrolled by default, but transparently switches to
controlled mode when value/onChange are supplied — one implementation, no behavioral divergence
between the two modes. Pass value as anything other than undefined to switch it to controlled:
import { useControllableState } from "@alphinex/hooks";
interface TabsProps {
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
}
function Tabs({ value, defaultValue = "overview", onChange }: TabsProps) {
const [activeTab, setActiveTab] = useControllableState({
value,
defaultValue,
onChange,
});
return (
<div role="tablist">
<button onClick={() => setActiveTab("overview")} aria-selected={activeTab === "overview"}>
Overview
</button>
{/* ... */}
</div>
);
}
// Uncontrolled: <Tabs defaultValue="settings" />
// Controlled: <Tabs value={tab} onChange={setTab} />In uncontrolled mode, setValue accepts a plain value or a functional updater, same as useState.
In controlled mode, calling setValue never touches internal state — it just resolves the next value
(applying your updater against the current controlled value if you passed a function) and calls
onChange with it; the parent owns re-rendering with the new value.
useMediaQuery
Reactively tracks a CSS media query via useSyncExternalStore — SSR-safe, returns false until the
client can evaluate window.matchMedia:
import { useMediaQuery } from "@alphinex/hooks";
function ResponsiveNav() {
const isDesktop = useMediaQuery("(min-width: 48rem)");
return isDesktop ? <DesktopNav /> : <MobileNav />;
}useDebouncedValue
Returns value, updated only after it has been stable for delayMs — the standard pattern for
debouncing a search input before firing a request:
import { useState } from "react";
import { useDebouncedValue } from "@alphinex/hooks";
function SearchBox() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebouncedValue(query, 300);
// effect/query hook keyed on debouncedQuery, not query
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}useClickOutside
Fires handler on any pointer event (mouse or touch) outside the returned ref's element — used for
dismissing menus, popovers, and dropdowns. Pass enabled = false to disable the listener without
unmounting (e.g. while the target is already closed):
import { useClickOutside } from "@alphinex/hooks";
function DropdownMenu({ onClose }: { onClose: () => void }) {
const ref = useClickOutside<HTMLDivElement>(onClose);
return <div ref={ref}>{/* menu items */}</div>;
}useLocalStorage
Persists a piece of state to localStorage, JSON-serialized, with the same tuple shape as
useState. SSR-safe — falls back to defaultValue when window isn't available, and silently keeps
working in-memory if storage is unavailable (private browsing, quota exceeded):
import { useLocalStorage } from "@alphinex/hooks";
function SidebarCollapseToggle() {
const [collapsed, setCollapsed] = useLocalStorage("sidebar-collapsed", false);
return (
<button onClick={() => setCollapsed((prev) => !prev)}>
{collapsed ? "Expand" : "Collapse"}
</button>
);
}usePrevious
Returns the value from the previous render — undefined on the first render. Useful for comparing
against a prop/state change (e.g. to animate only on transition):
import { usePrevious } from "@alphinex/hooks";
function Counter({ count }: { count: number }) {
const previousCount = usePrevious(count);
const direction = previousCount === undefined ? null : count > previousCount ? "up" : "down";
return <span data-direction={direction}>{count}</span>;
}See documentation/ARCHITECTURE.md for the full package contract, dependency rules, and roadmap placement.
