# usehooks-ts
usehooks-ts is a comprehensive React hooks library written in TypeScript that provides 33 production-ready custom hooks for common development patterns. Built around the DRY (Don't Repeat Yourself) principle, this library offers extensively tested, type-safe hooks that work seamlessly with React 16.8+ through React 19. The project is structured as a Turborepo monorepo using pnpm workspaces, containing the core hooks library and a Next.js documentation website with interactive demos.
The library is fully tree-shakable with minimal bundle impact, SSR-compatible, and published as the npm package `usehooks-ts`. Each hook follows React best practices with proper cleanup, memoization, and composability. The project demonstrates excellent TypeScript practices, comprehensive test coverage using Vitest, and modern build tooling with dual ESM/CJS outputs via tsup. With features like cross-tab synchronization for storage hooks, custom serialization support, and SSR initialization options, usehooks-ts is designed for production environments while maintaining developer-friendly APIs and thorough documentation.
## Installation and Setup
```bash
npm install usehooks-ts
```
```typescript
// Named imports (tree-shakable)
import { useLocalStorage, useCounter, useMediaQuery } from 'usehooks-ts'
```
## useLocalStorage
Persist state in browser localStorage with automatic cross-tab synchronization and SSR support.
```typescript
import { useLocalStorage } from 'usehooks-ts'
function ShoppingCart() {
// Basic usage with automatic JSON serialization
const [cart, setCart, removeCart] = useLocalStorage('cart', [])
// With custom serialization
const [user, setUser, removeUser] = useLocalStorage(
'user',
null,
{
serializer: (value) => JSON.stringify(value),
deserializer: (value) => JSON.parse(value),
initializeWithValue: false // For SSR - prevents reading localStorage on server
}
)
const addItem = (product) => {
setCart((prevCart) => [...prevCart, product])
}
const clearCart = () => {
removeCart() // Removes key from localStorage and resets to initial value
}
return (
Cart Items: {cart.length}
{JSON.stringify(cart, null, 2)}
)
}
// The hook automatically syncs across tabs - changes in one tab
// are reflected in all other tabs with the same localStorage key
```
## useCounter
Manage counter state with increment, decrement, reset, and custom setter functions.
```typescript
import { useCounter } from 'usehooks-ts'
function CounterDemo() {
const { count, increment, decrement, reset, setCount } = useCounter(0)
const incrementBy = (value) => {
setCount((x) => x + value)
}
const multiplyBy2 = () => {
setCount((x) => x * 2)
}
const setToRandom = () => {
setCount(Math.floor(Math.random() * 100))
}
return (
)}
>
)
}
```
## useEventListener
Attach event listeners to DOM elements, window, or document with automatic cleanup.
```typescript
import { useRef } from 'react'
import { useEventListener } from 'usehooks-ts'
function EventListenerDemo() {
const buttonRef = useRef(null)
const containerRef = useRef(null)
const documentRef = useRef(document)
// Window-level event
const handleScroll = (event) => {
console.log('Window scrolled:', window.scrollY)
}
// Document-level event
const handleVisibilityChange = (event) => {
console.log('Tab visibility changed:', !document.hidden)
}
// Element-level event with options
const handleClick = (event) => {
console.log('Button clicked:', event.target)
}
const handleMouseMove = (event) => {
console.log('Mouse moved in container:', event.clientX, event.clientY)
}
// Attach to window
useEventListener('scroll', handleScroll)
// Attach to document
useEventListener('visibilitychange', handleVisibilityChange, documentRef)
// Attach to specific element
useEventListener('click', handleClick, buttonRef)
// With event listener options
useEventListener('mousemove', handleMouseMove, containerRef, {
passive: true,
capture: false
})
return (
Scroll the page and check console
)
}
```
## useMediaQuery
Track media query state changes with SSR support and system preference detection.
```typescript
import { useMediaQuery } from 'usehooks-ts'
function ResponsiveLayout() {
const isMobile = useMediaQuery('(max-width: 768px)')
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)')
const isDesktop = useMediaQuery('(min-width: 1025px)')
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)')
const isPortrait = useMediaQuery('(orientation: portrait)')
// With SSR support
const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)', {
defaultValue: false, // Default value for SSR
initializeWithValue: false // Don't read on server
})
return (
)
}
```
## useHover
Track hover state of DOM elements with ref-based detection.
```typescript
import { useRef } from 'react'
import { useHover } from 'usehooks-ts'
function HoverCard() {
const hoverRef = useRef(null)
const isHover = useHover(hoverRef)
return (
{isHover ? 'Hovering! 🎯' : 'Hover over me'}
Hover state: {isHover ? 'true' : 'false'}
)
}
function Tooltip({ children, text }) {
const tooltipRef = useRef(null)
const isHover = useHover(tooltipRef)
return (
{children}
{isHover && (
{text}
)}
)
}
```
## useDebounceValue
Debounce value changes (different from useDebounceCallback which debounces function calls).
```typescript
import { useState } from 'react'
import { useDebounceValue } from 'usehooks-ts'
function LiveSearch() {
const [searchTerm, setSearchTerm] = useState('')
const [debouncedSearch] = useDebounceValue(searchTerm, 500)
// This effect runs only when debounced value changes
useEffect(() => {
if (debouncedSearch) {
fetch(`/api/search?q=${debouncedSearch}`)
.then(res => res.json())
.then(data => console.log('Search results:', data))
}
}, [debouncedSearch])
return (
setSearchTerm(e.target.value)}
placeholder="Type to search..."
/>
Actual value: {searchTerm}
Debounced value: {debouncedSearch}
API will be called with: {debouncedSearch || 'nothing yet'}
)
}
```
## useOnClickOutside
Detect clicks outside of specified elements for dropdown/modal closing.
```typescript
import { useRef } from 'react'
import { useOnClickOutside } from 'usehooks-ts'
function Dropdown() {
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef(null)
useOnClickOutside(dropdownRef, () => {
setIsOpen(false)
})
return (
)
}
```
## Summary
usehooks-ts provides a comprehensive suite of production-ready React hooks that cover the most common development patterns: state management (useBoolean, useCounter, useToggle, useStep), data persistence (useLocalStorage, useSessionStorage), timing operations (useInterval, useTimeout, useDebounceCallback), event handling (useEventListener, useClickAnyWhere, useOnClickOutside), DOM observation (useIntersectionObserver, useResizeObserver, useHover), responsive design (useMediaQuery, useWindowSize), theming (useDarkMode, useTernaryDarkMode), and browser APIs (useCopyToClipboard, useScript, useScreen). Each hook is designed with TypeScript type safety, SSR compatibility, and proper cleanup handling, making them suitable for modern React applications including Next.js, Remix, and other SSR frameworks.
The library excels at composability, allowing hooks to work together seamlessly (as seen with useDarkMode combining useMediaQuery, useLocalStorage, and useIsomorphicLayoutEffect). With features like cross-tab synchronization for storage hooks, custom serialization options, debounce/throttle controls, and intersection observer support, usehooks-ts eliminates boilerplate code while maintaining flexibility. The tree-shakable ESM build ensures minimal bundle impact, while comprehensive TypeScript definitions provide excellent developer experience. Whether building responsive layouts, implementing dark mode, managing form state, lazy loading content, or handling complex user interactions, usehooks-ts provides battle-tested solutions that follow React best practices and integrate smoothly into any React codebase.