@wt/hooks (0.0.2)

Published 2026-04-11 12:33:45 +02:00 by wess in wesnetech/hooks-pkg

Installation

@wt:registry=
npm install @wt/hooks@0.0.2
"@wt/hooks": "0.0.2"

About this package

Hooks

Shared React hooks library used across projects

Installation

yarn add @wt/hooks

# or

npm install @wt/hooks

Requirements

Peer dependency Version
react >= 18.0.0
react-router-dom >= 6.0.0 (only required if using useQuery)

Hooks

Hook Description
useLocalStorage State synchronized with localStorage, with cross-tab sync
useWindowResize Window dimensions and Tailwind-aligned breakpoint flags
useDeviceInfo Device type detection (mobile / tablet / desktop)
useQuery Parse URL query parameters via react-router-dom
useSearch Debounced search orchestration with minimum character threshold
useAddIdPrefix Format a numeric ID as a zero-padded #-prefixed string
useToggle Boolean state with a convenience toggle function
useDebounce Debounce any value by a configurable delay
useKeyPress Run a callback when a specific key is pressed
useEnterKeyPress Run a callback when Enter is pressed
useEscapeKeyPress Run a callback when Escape is pressed
useEventListener Attach event listeners to window or a specific element
useIsMounted Check whether the component is still mounted
useIsomorphicLayoutEffect useLayoutEffect in the browser, useEffect on the server

API Reference

useLocalStorage

function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void]

State that persists to localStorage. The setter accepts a direct value or a function updater, just like useState. Automatically syncs across tabs via the storage event and across hooks in the same tab via a custom local-storage event.

import { useLocalStorage } from '@wt/hooks';

function Counter() {
	const [count, setCount] = useLocalStorage('counter', 0);

	return (
		<button onClick={() => setCount((prev) => prev + 1)}>
			Count: {count}
		</button>
	);
}

useWindowResize

function useWindowResize(): {
	width: number;
	height: number;
	isSmallDevice: boolean;   // width <= 768
	isSmallerDevice: boolean; // width <= 480
	isXXS: boolean;           // width < 375
	isXS: boolean;            // width >= 375 && < 640
	isSM: boolean;            // width >= 640 && < 768
	isMD: boolean;            // width >= 768 && < 1024
	isLG: boolean;            // width >= 1024 && < 1280
	isXL: boolean;            // width >= 1280
}

Returns the current window dimensions and Tailwind-aligned breakpoint flags. Updates on resize with a 100ms debounce. SSR-safe (all values default to 0 / false).

import { useWindowResize } from '@wt/hooks';

function Layout() {
	const { width, isSmallDevice, isMD } = useWindowResize();

	if (isSmallDevice) return <MobileNav />;
	return <DesktopNav />;
}

useDeviceInfo

function useDeviceInfo(): {
	width: number;
	height: number;
	device: DeviceInfo; // { isMobile, isTablet, isDesktop, type }
}

Returns window dimensions and a device object with mobile/tablet/desktop classification. Breakpoints: mobile <= 768, tablet <= 1024, desktop > 1024. Updates on resize with a 100ms debounce. SSR-safe.

import { useDeviceInfo } from '@wt/hooks';

function App() {
	const { device } = useDeviceInfo();

	return <p>Device type: {device.type}</p>; // "mobile" | "tablet" | "desktop"
}

useQuery

function useQuery(): URLSearchParams

Returns a URLSearchParams object for the current URL's query string. Must be used inside a react-router-dom <Router> context.

import { useQuery } from '@wt/hooks';

function SearchResults() {
	const query = useQuery();
	const page = query.get('page'); // "2" for ?page=2

	return <p>Page: {page}</p>;
}

useSearch

function useSearch(
	query: string,
	minCharactersCallback: () => void,
	searchFunction: () => void,
): { finishedSearch: boolean }

Search orchestration hook. When query has fewer than 3 characters, minCharactersCallback is called immediately (use this to clear results or hide loading states). When query has 3+ characters, searchFunction is called after a 500ms debounce. Returns finishedSearch to indicate when the search has fired.

import { useSearch } from '@wt/hooks';

function SearchPage() {
	const [query, setQuery] = useState('');
	const [results, setResults] = useState([]);

	const { finishedSearch } = useSearch(
		query,
		() => setResults([]),
		() => fetchResults(query).then(setResults),
	);

	return (
		<div>
			<input value={query} onChange={(e) => setQuery(e.target.value)} />
			{!finishedSearch && query.length >= 3 && <Spinner />}
			{results.map((r) => <Result key={r.id} data={r} />)}
		</div>
	);
}

useAddIdPrefix

function useAddIdPrefix(): (id: number) => string

Returns a formatting function that takes a numeric ID and returns it as a #-prefixed string, zero-padded to 6 digits.

import { useAddIdPrefix } from '@wt/hooks';

function OrderRow({ orderId }: { orderId: number }) {
	const addPrefix = useAddIdPrefix();

	return <td>{addPrefix(orderId)}</td>; // 25 → "#000025"
}

useToggle

function useToggle(initialState?: boolean): [
	boolean,
	React.Dispatch<React.SetStateAction<boolean>>,
	() => void,
]

Boolean state hook that returns a 3-tuple: [state, setState, toggle]. setState is the standard React setter (for setting an explicit value), and toggle is a convenience function that flips the current state. Defaults to false.

import { useToggle } from '@wt/hooks';

function Modal() {
	const [isOpen, setIsOpen, toggleOpen] = useToggle();

	return (
		<div>
			<button onClick={toggleOpen}>Toggle</button>
			<button onClick={() => setIsOpen(true)}>Force open</button>
			{isOpen && <div className="modal">Content</div>}
		</div>
	);
}

useDebounce

function useDebounce<T>(value: T, delay?: number): T

Returns a debounced version of value that only updates after delay milliseconds of inactivity. Defaults to 500ms.

import { useDebounce } from '@wt/hooks';

function Search() {
	const [input, setInput] = useState('');
	const debouncedInput = useDebounce(input, 300);

	useEffect(() => {
		if (debouncedInput) fetchResults(debouncedInput);
	}, [debouncedInput]);

	return <input value={input} onChange={(e) => setInput(e.target.value)} />;
}

useKeyPress

function useKeyPress(target: string, callback: () => void): void

Listens for keydown events on document and calls callback when event.key matches target.

import { useKeyPress } from '@wt/hooks';

function Player() {
	useKeyPress(' ', () => togglePlayback());
	useKeyPress('ArrowRight', () => skipForward());

	return <Video />;
}

useEnterKeyPress

function useEnterKeyPress(callback: () => void): void

Calls callback when the Enter key is pressed. Shorthand for useKeyPress('Enter', callback).

import { useEnterKeyPress } from '@wt/hooks';

function ChatInput() {
	const [message, setMessage] = useState('');
	useEnterKeyPress(() => sendMessage(message));

	return <input value={message} onChange={(e) => setMessage(e.target.value)} />;
}

useEscapeKeyPress

function useEscapeKeyPress(callback: () => void): void

Calls callback when the Escape key is pressed. Shorthand for useKeyPress('Escape', callback).

import { useEscapeKeyPress } from '@wt/hooks';

function Dialog({ onClose }: { onClose: () => void }) {
	useEscapeKeyPress(onClose);

	return <div className="dialog">Content</div>;
}

useEventListener

// Listen on window
function useEventListener<K extends keyof WindowEventMap>(
	eventName: K,
	handler: (event: WindowEventMap[K]) => void,
): void

// Listen on a specific element
function useEventListener<K extends keyof HTMLElementEventMap, T extends HTMLElement>(
	eventName: K,
	handler: (event: HTMLElementEventMap[K]) => void,
	element: RefObject<T>,
): void

Type-safe event listener that attaches to window by default, or to a specific element when a RefObject is provided. The handler is stored in a ref to avoid stale closures. Cleans up automatically on unmount.

import { useEventListener } from '@wt/hooks';

// Window event
function ScrollTracker() {
	useEventListener('scroll', () => {
		console.log(window.scrollY);
	});

	return null;
}

// Element event
function Hoverable() {
	const ref = useRef<HTMLDivElement>(null);
	useEventListener('mouseenter', () => console.log('hovered'), ref);

	return <div ref={ref}>Hover me</div>;
}

useIsMounted

function useIsMounted(): () => boolean

Returns a stable callback that returns true while the component is mounted and false after unmount. Useful for guarding async operations.

import { useIsMounted } from '@wt/hooks';

function UserProfile({ userId }: { userId: string }) {
	const isMounted = useIsMounted();
	const [user, setUser] = useState(null);

	useEffect(() => {
		fetchUser(userId).then((data) => {
			if (isMounted()) setUser(data);
		});
	}, [userId]);

	return user ? <p>{user.name}</p> : <Spinner />;
}

useIsomorphicLayoutEffect

const useIsomorphicLayoutEffect: typeof useLayoutEffect

Resolves to useLayoutEffect in the browser and useEffect on the server, avoiding the SSR warning React emits for useLayoutEffect. Same API as useEffect.

import { useIsomorphicLayoutEffect } from '@wt/hooks';

function Tooltip({ targetRef }) {
	useIsomorphicLayoutEffect(() => {
		// Measure DOM before paint
		const rect = targetRef.current.getBoundingClientRect();
		positionTooltip(rect);
	}, [targetRef]);

	return <div className="tooltip">Tip</div>;
}

Types

The package exports the following types:

type DeviceType = 'mobile' | 'tablet' | 'desktop';

interface DeviceInfo {
	isMobile: boolean;
	isTablet: boolean;
	isDesktop: boolean;
	type: DeviceType;
}

Development

yarn install    # Install dependencies
yarn test       # Run tests (vitest)
yarn build      # Compile TypeScript

Dependencies

Development dependencies

ID Version
@testing-library/dom ^10.4.1
@testing-library/react ^16.3.2
@types/node ^22.0.0
@types/react ^18.0.0
jsdom ^29.0.2
react ^18.0.0
react-dom ^18.0.0
react-router-dom ^7.14.0
rimraf ^6.0.1
typescript ^4.4.2
vitest ^4.1.4

Peer dependencies

ID Version
react >= 18.0.0
react-router-dom >= 6.0.0
Details
npm
2026-04-11 12:33:45 +02:00
4
MIT
latest
14 KiB
Assets (1)
Versions (1) View all
0.0.2 2026-04-11