# 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 (

Count: {count}

) } ``` ## useBoolean Handle boolean state with utility functions for common operations. ```typescript import { useBoolean } from 'usehooks-ts' function ModalDialog() { const { value: isOpen, setTrue: open, setFalse: close, toggle } = useBoolean(false) const { value: isLoading, setTrue: startLoading, setFalse: stopLoading } = useBoolean(false) const handleSubmit = async () => { startLoading() try { await fetch('/api/submit', { method: 'POST' }) close() } catch (error) { console.error(error) } finally { stopLoading() } } return ( <> {isOpen && (

Dialog Title

)} ) } ``` ## 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 (

Responsive Layout

Device type: {isMobile ? 'Mobile' : isTablet ? 'Tablet' : 'Desktop'}

Orientation: {isPortrait ? 'Portrait' : 'Landscape'}

Dark mode: {isDarkMode ? 'Enabled' : 'Disabled'}

Reduced motion: {prefersReducedMotion ? 'Yes' : 'No'}

{isMobile && } {isDesktop && }
) } ``` ## useIntersectionObserver Track element visibility using the Intersection Observer API with threshold control and freeze options. ```typescript import { useIntersectionObserver } from 'usehooks-ts' function LazyImage({ src, alt }) { // Supports both array and object destructuring const { ref, isIntersecting, entry } = useIntersectionObserver({ threshold: 0.5, // Trigger when 50% visible root: null, // viewport rootMargin: '0px', freezeOnceVisible: true, // Stop observing after first intersection onChange: (isIntersecting, entry) => { console.log('Visibility changed:', isIntersecting, entry.intersectionRatio) }, initialIsIntersecting: false }) return (
{isIntersecting ? ( {alt} ) : (
Loading...
)} {entry && (

Intersection ratio: {(entry.intersectionRatio * 100).toFixed(0)}%

)}
) } function InfiniteScroll() { // Using array destructuring const [ref, isIntersecting] = useIntersectionObserver({ threshold: 0.1, rootMargin: '100px' // Load before scrolling to element }) useEffect(() => { if (isIntersecting) { loadMoreItems() } }, [isIntersecting]) return (
{items.map(item => )}
{isIntersecting && }
) } ``` ## useDarkMode Manage dark mode state with system preference detection and localStorage persistence. ```typescript import { useDarkMode } from 'usehooks-ts' function ThemeToggle() { const { isDarkMode, toggle, enable, disable, set } = useDarkMode({ defaultValue: false, localStorageKey: 'app-theme', // Custom storage key initializeWithValue: true // Read from localStorage on mount }) // Apply theme to document useEffect(() => { document.documentElement.classList.toggle('dark', isDarkMode) document.documentElement.style.colorScheme = isDarkMode ? 'dark' : 'light' }, [isDarkMode]) return (

Current theme: {isDarkMode ? 'Dark Mode' : 'Light Mode'}

Automatically syncs with system preference

) } ``` ## useDebounceCallback Create debounced callback functions with control over timing and invocation behavior. ```typescript import { useState } from 'react' import { useDebounceCallback } from 'usehooks-ts' function SearchBar() { const [searchTerm, setSearchTerm] = useState('') const [results, setResults] = useState([]) const debouncedSearch = useDebounceCallback( async (term) => { if (!term) { setResults([]) return } try { const response = await fetch(`/api/search?q=${term}`) const data = await response.json() setResults(data) } catch (error) { console.error('Search failed:', error) } }, 500, // Wait 500ms after last keystroke { leading: false, // Don't invoke on leading edge trailing: true, // Invoke on trailing edge maxWait: 2000 // Maximum wait time } ) const handleInputChange = (e) => { const value = e.target.value setSearchTerm(value) debouncedSearch(value) } const handleCancel = () => { debouncedSearch.cancel() // Cancel pending invocation setResults([]) } const handleSearchNow = () => { debouncedSearch.flush() // Immediately invoke pending call } const isPending = debouncedSearch.isPending() return (
{isPending && Searching...}
) } ``` ## useInterval Execute callback functions at regular intervals with proper cleanup and dynamic delay control. ```typescript import { useState } from 'react' import { useInterval } from 'usehooks-ts' function Timer() { const [count, setCount] = useState(0) const [delay, setDelay] = useState(1000) const [isRunning, setIsRunning] = useState(true) // Pass null to stop the interval useInterval( () => { setCount(c => c + 1) }, isRunning ? delay : null ) return (

Timer: {count}s

) } function DataPolling() { const [data, setData] = useState(null) const [lastUpdate, setLastUpdate] = useState(new Date()) // Poll API every 5 seconds useInterval(async () => { try { const response = await fetch('/api/data') const json = await response.json() setData(json) setLastUpdate(new Date()) } catch (error) { console.error('Polling failed:', error) } }, 5000) return (

Last updated: {lastUpdate.toLocaleTimeString()}

{JSON.stringify(data, null, 2)}
) } ``` ## useCopyToClipboard Copy text to clipboard using the Clipboard API with success/failure tracking. ```typescript import { useCopyToClipboard } from 'usehooks-ts' function CopyButton({ text }) { const [copiedText, copy] = useCopyToClipboard() const [showFeedback, setShowFeedback] = useState(false) const handleCopy = async () => { const success = await copy(text) if (success) { setShowFeedback(true) setTimeout(() => setShowFeedback(false), 2000) } else { alert('Failed to copy to clipboard') } } return (
{copiedText &&

Last copied: {copiedText}

}
) } function CodeBlock({ code }) { const [copiedText, copy] = useCopyToClipboard() const handleCopy = async () => { const success = await copy(code) console.log(success ? 'Code copied!' : 'Copy failed') } return (
{code}
) } ``` ## useMap Manage Map data structure state with React state management patterns. ```typescript import { useMap } from 'usehooks-ts' function UserCache() { const [users, { set, setAll, remove, reset }] = useMap( new Map([ [1, { id: 1, name: 'Alice' }], [2, { id: 2, name: 'Bob' }] ]) ) const addUser = (user) => { set(user.id, user) } const updateUser = (id, updates) => { const user = users.get(id) if (user) { set(id, { ...user, ...updates }) } } const deleteUser = (id) => { remove(id) } const bulkAddUsers = (userArray) => { const entries = userArray.map(user => [user.id, user]) setAll(new Map(entries)) } const clearCache = () => { reset() // Clears all entries } return (

User Cache ({users.size} users)

) } ``` ## useSessionStorage Persist state in sessionStorage (similar to useLocalStorage but session-scoped). ```typescript import { useSessionStorage } from 'usehooks-ts' function MultiStepForm() { const [formData, setFormData, removeFormData] = useSessionStorage('form-data', { step: 1, personal: {}, contact: {}, preferences: {} }) const updateStep = (step, data) => { setFormData(prev => ({ ...prev, step: step + 1, [step === 1 ? 'personal' : step === 2 ? 'contact' : 'preferences']: data })) } const resetForm = () => { removeFormData() // Clears sessionStorage and resets to initial value } return (

Step {formData.step} of 3

{formData.step === 1 && ( updateStep(1, data)} /> )} {formData.step === 2 && ( updateStep(2, data)} /> )} {formData.step === 3 && ( { updateStep(3, data) // Submit complete form }} /> )}
) } ``` ## useWindowSize Track window dimensions with optional debouncing for performance optimization. ```typescript import { useWindowSize } from 'usehooks-ts' function ResponsiveComponent() { const { width, height } = useWindowSize({ initializeWithValue: true, debounceDelay: 150 // Debounce resize events }) const getBreakpoint = () => { if (width < 640) return 'mobile' if (width < 1024) return 'tablet' return 'desktop' } return (

Window Size Tracker

Width: {width}px

Height: {height}px

Breakpoint: {getBreakpoint()}

Aspect Ratio: {(width / height).toFixed(2)}

{width < 640 && } {width >= 640 && width < 1024 && } {width >= 1024 && }
) } ``` ## 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 (
{isOpen && (
Profile Settings Logout
)}
) } function Modal({ isOpen, onClose, children }) { const modalRef = useRef(null) useOnClickOutside(modalRef, onClose) if (!isOpen) return null return (
{children}
) } ``` ## useStep Manage multi-step process navigation with boundary checks and helpers. ```typescript import { useStep } from 'usehooks-ts' function Wizard() { const [currentStep, { goToNextStep, goToPrevStep, reset, canGoToNextStep, canGoToPrevStep, setStep }] = useStep(4) const steps = [ { id: 1, title: 'Personal Info', component: PersonalInfo }, { id: 2, title: 'Address', component: Address }, { id: 3, title: 'Payment', component: Payment }, { id: 4, title: 'Review', component: Review } ] const CurrentStepComponent = steps[currentStep - 1].component return (
{steps.map((step) => (
setStep(step.id)} > {step.title}
))}

Step {currentStep} of {steps.length}

) } ``` ## useScript Dynamically load external scripts with status tracking. ```typescript import { useScript } from 'usehooks-ts' function GoogleMapsComponent() { const status = useScript( `https://maps.googleapis.com/maps/api/js?key=${API_KEY}`, { removeOnUnmount: false // Keep script loaded after unmount } ) useEffect(() => { if (status === 'ready') { // Initialize map const map = new google.maps.Map(document.getElementById('map'), { center: { lat: -34.397, lng: 150.644 }, zoom: 8 }) } }, [status]) return (
{status === 'loading' &&

Loading Google Maps...

} {status === 'error' &&

Failed to load Google Maps

} {status === 'ready' &&
}
) } function StripePayment() { const stripeStatus = useScript('https://js.stripe.com/v3/') const [stripe, setStripe] = useState(null) useEffect(() => { if (stripeStatus === 'ready' && window.Stripe) { setStripe(window.Stripe(PUBLISHABLE_KEY)) } }, [stripeStatus]) return stripe ? : } ``` ## useIsClient Detect client-side rendering for SSR-safe operations. ```typescript import { useIsClient } from 'usehooks-ts' function ClientOnlyComponent() { const isClient = useIsClient() if (!isClient) { return
Server-side render placeholder
} // Safe to use window, document, localStorage, etc. return (

Window width: {window.innerWidth}px

User agent: {navigator.userAgent}

Local storage available: {typeof localStorage !== 'undefined' ? 'Yes' : 'No'}

) } function ConditionalRender() { const isClient = useIsClient() return (

My App

{isClient && } {isClient ? : }
) } ``` ## Complete Application Example ```typescript import { useLocalStorage, useMediaQuery, useDarkMode, useDebounceCallback, useIntersectionObserver, useOnClickOutside } from 'usehooks-ts' function TodoApp() { // State management with persistence const [todos, setTodos] = useLocalStorage('todos', []) const [searchTerm, setSearchTerm] = useState('') // Theme management const { isDarkMode, toggle } = useDarkMode() const isMobile = useMediaQuery('(max-width: 768px)') // Debounced search const debouncedSearch = useDebounceCallback( (term) => { console.log('Searching for:', term) // Perform search logic }, 300 ) // Lazy loading for todo items const { ref: lastItemRef, isIntersecting } = useIntersectionObserver({ threshold: 0.1, rootMargin: '100px' }) // Dropdown menu const [isMenuOpen, setIsMenuOpen] = useState(false) const menuRef = useRef(null) useOnClickOutside(menuRef, () => setIsMenuOpen(false)) // Load more todos when scrolling useEffect(() => { if (isIntersecting) { loadMoreTodos() } }, [isIntersecting]) const addTodo = (text) => { setTodos([...todos, { id: Date.now(), text, completed: false, createdAt: new Date().toISOString() }]) } const toggleTodo = (id) => { setTodos(todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo )) } const deleteTodo = (id) => { setTodos(todos.filter(todo => todo.id !== id)) } const handleSearchChange = (e) => { const value = e.target.value setSearchTerm(value) debouncedSearch(value) } const filteredTodos = todos.filter(todo => todo.text.toLowerCase().includes(searchTerm.toLowerCase()) ) return (

Todo App

{isMenuOpen && ( )}
    {filteredTodos.map((todo, index) => (
  • toggleTodo(todo.id)} /> {todo.text}
  • ))}
{isIntersecting &&
Loading more...
}
) } ``` ## 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.