React Hooks
A collection of custom React hooks for common use cases.
useLocalStorage
Store and retrieve values from localStorage with React state synchronization.
import { useState, useEffect } from 'react'
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue
}
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.error(error)
return initialValue
}
})
const setValue = (value: T) => {
try {
setStoredValue(value)
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(value))
}
} catch (error) {
console.error(error)
}
}
return [storedValue, setValue]
}
// Usage
function Component() {
const [name, setName] = useLocalStorage('name', 'Guest')
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
)
}useMediaQuery
Detect media query matches.
import { useState, useEffect } from 'react'
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false)
useEffect(() => {
const media = window.matchMedia(query)
if (media.matches !== matches) {
setMatches(media.matches)
}
const listener = () => setMatches(media.matches)
media.addEventListener('change', listener)
return () => media.removeEventListener('change', listener)
}, [matches, query])
return matches
}
// Usage
function ResponsiveComponent() {
const isMobile = useMediaQuery('(max-width: 768px)')
return (
<div>
{isMobile ? 'Mobile View' : 'Desktop View'}
</div>
)
}useOnClickOutside
Detect clicks outside of an element.
import { useEffect, RefObject } from 'react'
export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
ref: RefObject<T>,
handler: (event: MouseEvent | TouchEvent) => void
) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
const el = ref?.current
if (!el || el.contains(event.target as Node)) {
return
}
handler(event)
}
document.addEventListener('mousedown', listener)
document.addEventListener('touchstart', listener)
return () => {
document.removeEventListener('mousedown', listener)
document.removeEventListener('touchstart', listener)
}
}, [ref, handler])
}
// Usage
function Modal() {
const modalRef = useRef<HTMLDivElement>(null)
const [isOpen, setIsOpen] = useState(false)
useOnClickOutside(modalRef, () => setIsOpen(false))
return isOpen ? (
<div ref={modalRef}>
Modal Content
</div>
) : null
}useFetch
Simple data fetching hook with loading and error states.
import { useState, useEffect } from 'react'
interface FetchState<T> {
data: T | null
loading: boolean
error: Error | null
}
export function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null,
})
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url)
if (!response.ok) throw new Error('Network response was not ok')
const data = await response.json()
setState({ data, loading: false, error: null })
} catch (error) {
setState({ data: null, loading: false, error: error as Error })
}
}
fetchData()
}, [url])
return state
}
// Usage
function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useFetch<User>(`/api/users/${userId}`)
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
if (!data) return null
return <div>{data.name}</div>
}useToggle
Toggle boolean state easily.
import { useState, useCallback } from 'react'
export function useToggle(initialValue: boolean = false): [boolean, () => void] {
const [value, setValue] = useState(initialValue)
const toggle = useCallback(() => setValue(v => !v), [])
return [value, toggle]
}
// Usage
function Component() {
const [isVisible, toggleVisible] = useToggle(false)
return (
<div>
<button onClick={toggleVisible}>Toggle</button>
{isVisible && <div>Content</div>}
</div>
)
}usePrevious
Get the previous value of a state or prop.
import { useEffect, useRef } from 'react'
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
// Usage
function Counter() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
)
}useWindowSize
Track window dimensions.
import { useState, useEffect } from 'react'
interface WindowSize {
width: number
height: number
}
export function useWindowSize(): WindowSize {
const [windowSize, setWindowSize] = useState<WindowSize>({
width: 0,
height: 0,
})
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
handleResize()
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return windowSize
}
// Usage
function Component() {
const { width, height } = useWindowSize()
return (
<div>
Window size: {width} x {height}
</div>
)
}useCopyToClipboard
Copy text to clipboard.
import { useState } from 'react'
export function useCopyToClipboard(): [string | null, (text: string) => Promise<void>] {
const [copiedText, setCopiedText] = useState<string | null>(null)
const copy = async (text: string) => {
if (!navigator?.clipboard) {
console.warn('Clipboard not supported')
return
}
try {
await navigator.clipboard.writeText(text)
setCopiedText(text)
} catch (error) {
console.error('Copy failed:', error)
setCopiedText(null)
}
}
return [copiedText, copy]
}
// Usage
function CopyButton({ text }: { text: string }) {
const [copiedText, copy] = useCopyToClipboard()
return (
<button onClick={() => copy(text)}>
{copiedText ? 'Copied!' : 'Copy'}
</button>
)
}All hooks are TypeScript-ready and production-tested. Feel free to use them in your projects!