A production-ready collection of fully-tested, zero-dependency React hooks. Each hook is completely independent, fully typed with TypeScript, and demonstrates advanced React patterns with comprehensive documentation.
- ๐ Production-Ready - Battle-tested in real applications
- ๐ฆ Zero Dependencies - Only requires React 18+
- ๐ฏ 100% TypeScript - Full type safety with excellent IntelliSense
- โ 98.25% Test Coverage - Comprehensive test suites included
- ๐ Fully Documented - JSDoc with examples for every hook
- ๐งน Memory Safe - Proper cleanup and no memory leaks
- โก Performance Optimized - RAF batching, memoization, deduplication
| Hook | Description | Tests | Coverage |
|---|---|---|---|
| useContainerSize | Track element dimensions and position with ResizeObserver | 17 | 98.66% |
| useDragResize | Drag-to-resize with constraints, aspect ratio, and grid snapping | 25 | 98.9% |
| useThrottle | Throttle function execution with leading/trailing edge control | 24 | 94.44% |
| useToast | Global toast notification system with pub-sub pattern | 24 | 100% |
Total: 90 passing tests, 98.25% overall coverage
npm install react-hooks-collectionimport { useContainerSize, useDragResize, useThrottle, useToast } from 'react-hooks-collection'
// Track container size
function ResponsiveChart() {
const { ref, width, height } = useContainerSize()
return <div ref={ref}>{width} x {height}</div>
}
// Drag-to-resize
function ResizablePanel() {
const { initiateResize, currentWidth, currentHeight } = useDragResize({
minWidth: 200,
minHeight: 100,
maxWidth: 800,
})
return (
<div style={{ width: currentWidth, height: currentHeight }}>
<div onPointerDown={initiateResize('right')} className="resize-handle" />
</div>
)
}
// Throttle function calls
function SearchBox() {
const throttledSearch = useThrottle((query: string) => {
console.log('Searching:', query)
}, 500)
return <input onChange={(e) => throttledSearch.run(e.target.value)} />
}
// Toast notifications
function App() {
const { toast } = useToast()
return (
<button onClick={() => toast({ title: 'Success!', variant: 'default' })}>
Show Toast
</button>
)
}Track element dimensions and position with ResizeObserver for responsive components.
Features:
- โ Real-time dimension tracking with ResizeObserver
- โ Position tracking with scroll parent detection
- โ RAF-based updates for smooth 60fps rendering
- โ Sub-pixel deduplication to prevent thrashing
- โ Proper cleanup on node changes and unmount
Usage:
import { useContainerSize } from 'react-hooks-collection'
function ResponsiveChart() {
const { ref, width, height, left, top } = useContainerSize()
return (
<div ref={ref} style={{ width: '100%', height: '400px' }}>
{width > 0 && (
<svg width={width} height={height}>
<rect width={width} height={height} fill="lightblue" />
<text x={10} y={20}>Size: {width} ร {height}</text>
<text x={10} y={40}>Position: ({left}, {top})</text>
</svg>
)}
</div>
)
}API:
interface ContainerSize {
width: number
height: number
left: number
top: number
}
function useContainerSize(): {
ref: (node: HTMLElement | null) => void
width: number
height: number
left: number
top: number
}Use Cases:
- Responsive charts and visualizations
- Canvas sizing based on container
- Dynamic layout calculations
- Responsive typography scaling
Enable drag-to-resize functionality with constraints, aspect ratio locking, and grid snapping.
Features:
- โ Bidirectional resize (left/right handles)
- โ Min/max width and height constraints
- โ Optional aspect ratio locking
- โ Grid snapping for aligned layouts
- โ Pointer capture for smooth dragging
- โ RAF batching for 60fps updates
- โ Live or trailing-only dimension callbacks
Usage:
import { useDragResize } from 'react-hooks-collection'
function ResizableImage({ src }) {
const {
initiateResize,
currentWidth,
currentHeight,
isResizing,
} = useDragResize({
initialWidth: 400,
initialHeight: 300,
minWidth: 200,
minHeight: 150,
maxWidth: 1000,
contentWidth: 16, // Lock to 16:9 aspect ratio
contentHeight: 9,
gridPercent: 25, // Snap to 25% increments
onDimensionsChange: (dims) => {
console.log('Resized to:', dims)
},
})
return (
<div style={{ position: 'relative', width: currentWidth, height: currentHeight }}>
<img src={src} style={{ width: '100%', height: '100%' }} />
{/* Right resize handle */}
<div
onPointerDown={initiateResize('right')}
style={{
position: 'absolute',
right: 0,
top: 0,
width: 8,
height: '100%',
cursor: 'ew-resize',
background: isResizing ? 'blue' : 'gray',
}}
/>
</div>
)
}API:
interface UseDragResizeParams {
initialWidth?: number
initialHeight?: number
minWidth: number
minHeight: number
maxWidth: number
maxHeight?: number
gridPercent?: number // 1-100, defaults to 100 (no snapping)
contentWidth?: number // For aspect ratio locking
contentHeight?: number // For aspect ratio locking
live?: boolean // Live updates during drag (default: false)
onDimensionsChange?: (dims: { width: number; height: number }) => void
}
function useDragResize(params: UseDragResizeParams): {
initiateResize: (direction: 'left' | 'right', boundaryWidth?: number) => (evt: React.PointerEvent) => void
isResizing: boolean
currentWidth: number
currentHeight: number
setDimensions: (dims: { width: number; height: number }) => void
}Advanced Example - With Aspect Ratio:
// 16:9 video player with constrained resize
const videoResize = useDragResize({
initialWidth: 640,
initialHeight: 360,
minWidth: 320,
minHeight: 180,
maxWidth: 1920,
contentWidth: 16,
contentHeight: 9, // Maintains 16:9 ratio
})Throttle function execution to limit call frequency with leading/trailing edge control.
Features:
- โ Configurable leading/trailing edge execution
- โ Cancel and flush methods for manual control
- โ Pending state checking
- โ No stale closures (callback ref updated)
- โ Automatic cleanup on unmount
Usage:
import { useThrottle } from 'react-hooks-collection'
function SearchInput() {
const [query, setQuery] = useState('')
const searchAPI = useCallback((searchTerm: string) => {
console.log('Searching for:', searchTerm)
// API call here...
}, [])
// Execute at most once per 500ms
const throttled = useThrottle(searchAPI, 500, {
leading: true, // Execute immediately on first call
trailing: true, // Execute once more at end if called during window
})
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setQuery(value)
throttled.run(value)
}
return (
<div>
<input value={query} onChange={handleChange} />
{throttled.pending() && <span>โณ Pending...</span>}
<button onClick={() => throttled.flush()}>Search Now</button>
<button onClick={() => throttled.cancel()}>Cancel</button>
</div>
)
}API:
interface ThrottleOptions {
leading?: boolean // Default: true
trailing?: boolean // Default: true
}
function useThrottle<T extends (...args: any[]) => any>(
callback: T,
delay: number,
options?: ThrottleOptions
): {
run: (...args: Parameters<T>) => void
cancel: () => void
flush: () => void
pending: () => boolean
}Edge Cases Handled:
// Zero delay - executes immediately
const immediate = useThrottle(fn, 0)
// Negative delay - executes immediately
const negative = useThrottle(fn, -100)
// Neither leading nor trailing - never executes
const never = useThrottle(fn, 1000, { leading: false, trailing: false })Scroll Handler Example:
function ScrollComponent() {
const handleScroll = useCallback(() => {
console.log('Scroll position:', window.scrollY)
}, [])
const throttled = useThrottle(handleScroll, 200)
useEffect(() => {
window.addEventListener('scroll', throttled.run)
return () => {
window.removeEventListener('scroll', throttled.run)
throttled.cancel() // Clean up pending calls
}
}, [throttled])
return <div>Scroll me!</div>
}Global toast notification system with pub-sub pattern for app-wide notifications.
Features:
- โ Global state (call from anywhere, even outside React)
- โ Toast limit enforcement (TOAST_LIMIT = 1)
- โ Auto-removal after dismiss
- โ Update existing toasts
- โ Dismiss specific or all toasts
- โ Variant support (default, destructive)
- โ Custom action buttons
Usage:
import { useToast, toast } from 'react-hooks-collection'
// In a component
function MyComponent() {
const { toast, dismiss } = useToast()
const handleSubmit = async () => {
try {
await submitForm()
toast({
title: 'Success!',
description: 'Form submitted successfully.',
})
} catch (error) {
toast({
variant: 'destructive',
title: 'Error',
description: error.message,
})
}
}
return (
<div>
<button onClick={handleSubmit}>Submit</button>
<button onClick={() => dismiss()}>Dismiss All</button>
</div>
)
}
// Or use imperatively (anywhere in your app, even outside React):
import { toast } from 'react-hooks-collection'
// From an API handler
fetch('/api/data')
.then(() => toast({ title: 'Data loaded!' }))
.catch(() => toast({ title: 'Error', variant: 'destructive' }))Toaster Component:
import { useToast } from 'react-hooks-collection'
export function Toaster() {
const { toasts } = useToast()
return (
<div className="toaster">
{toasts.map((t) => (
<div key={t.id} className={`toast toast-${t.variant}`}>
<h3>{t.title}</h3>
<p>{t.description}</p>
{t.action}
</div>
))}
</div>
)
}API:
interface ToastProps {
open?: boolean
onOpenChange?: (open: boolean) => void
variant?: 'default' | 'destructive'
}
interface ToasterToast extends ToastProps {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: React.ReactElement
}
function useToast(): {
toasts: ToasterToast[]
toast: (props: Partial<ToasterToast>) => { id: string; dismiss: () => void; update: (props: Partial<ToasterToast>) => void }
dismiss: (toastId?: string) => void
}
// Imperative API
function toast(props: Partial<ToasterToast>): {
id: string
dismiss: () => void
update: (props: Partial<ToasterToast>) => void
}Advanced Example - Update Toast:
function UploadComponent() {
const handleUpload = async (file: File) => {
const toastInstance = toast({
title: 'Uploading...',
description: `Uploading ${file.name}`,
})
try {
await uploadFile(file)
toastInstance.update({
title: 'Upload complete!',
description: `${file.name} uploaded successfully`,
})
} catch (error) {
toastInstance.update({
variant: 'destructive',
title: 'Upload failed',
description: error.message,
})
}
}
return <input type="file" onChange={(e) => handleUpload(e.target.files[0])} />
}All hooks include comprehensive test suites with 98.25% coverage.
# Run all unit tests
npm test
# Run tests in watch mode
npm run test:watch
# Generate coverage report
npm run test:coverageimport { renderHook, act } from '@testing-library/react'
import { useThrottle } from 'react-hooks-collection'
test('should throttle function calls', () => {
const callback = jest.fn()
const { result } = renderHook(() => useThrottle(callback, 1000))
// First call executes immediately
act(() => {
result.current.run('arg1')
})
expect(callback).toHaveBeenCalledTimes(1)
// Second call within window is throttled
act(() => {
jest.advanceTimersByTime(500)
result.current.run('arg2')
})
expect(callback).toHaveBeenCalledTimes(1)
// Trailing call executes after window
act(() => {
jest.advanceTimersByTime(500)
})
expect(callback).toHaveBeenCalledTimes(2)
expect(callback).toHaveBeenLastCalledWith('arg2')
})Interactive documentation and examples for all hooks are available in Storybook.
# Start Storybook development server
npm run storybook
# Build static Storybook
npm run build-storybookStorybook will start at http://localhost:6006 where you can:
- ๐จ Explore interactive demos - Try all hooks with live controls
- ๐ View documentation - See comprehensive usage examples
- ๐ฎ Test edge cases - Experiment with different configurations
- ๐ Inspect source code - View implementation details
Each hook includes 3 interactive stories:
useContainerSize:
- BasicUsage - Interactive container resizing with slider
- ResponsiveChart - SVG chart that adapts to container size
useDragResize:
- BasicResize - Simple resizable panel with handle
- AspectRatioLocked - 16:9 video player mockup
- GridSnapping - Visual grid with adjustable snap percentage
useThrottle:
- BasicUsage - Input field with call logging
- ScrollThrottle - Scroll event reduction metrics
- LeadingTrailingOptions - Configurable throttle behavior
useToast:
- BasicUsage - Success/error/action button toasts
- UpdateToast - File upload progress simulation
- ImperativeAPI - Global toast calls from anywhere
End-to-end tests verify hook functionality through real browser interactions.
# Run E2E tests (headless)
npm run test:e2e
# Run E2E tests with UI
npm run test:e2e:ui
# Run E2E tests in headed mode
npm run test:e2e:headedE2E tests validate:
- โ Real user interactions - Click, drag, scroll, type
- โ Visual rendering - Element visibility and styling
- โ State updates - Live dimension tracking, toast notifications
- โ Edge cases - Grid snapping, aspect ratio locking, throttle limits
- โ Accessibility - Keyboard navigation and ARIA attributes
import { test, expect } from '@playwright/test'
test('should show success toast when button clicked', async ({ page }) => {
await page.goto('/iframe.html?id=hooks-usetoast--basic-usage')
const successButton = page.locator('button:has-text("Show Success Toast")')
await successButton.click()
// Check for toast with success message
const toast = page.locator('text="Success!"')
await expect(toast).toBeVisible({ timeout: 5000 })
})Total E2E Tests: 30+ tests covering all hook interactions
function useResizableWithNotifications() {
const { toast } = useToast()
const resize = useDragResize({
minWidth: 200,
maxWidth: 1000,
onDimensionsChange: ({ width, height }) => {
toast({
title: 'Resized',
description: `New size: ${width} ร ${height}`,
})
},
})
return resize
}function useResponsive() {
const { width } = useContainerSize()
return {
isMobile: width < 768,
isTablet: width >= 768 && width < 1024,
isDesktop: width >= 1024,
}
}
function ResponsiveApp() {
const { ref, isMobile } = useResponsive()
return (
<div ref={ref}>
{isMobile ? <MobileLayout /> : <DesktopLayout />}
</div>
)
}function ScrollSpy() {
const [scrollY, setScrollY] = useState(0)
const handleScroll = useCallback(() => {
setScrollY(window.scrollY)
}, [])
const throttled = useThrottle(handleScroll, 100)
useEffect(() => {
window.addEventListener('scroll', throttled.run)
return () => {
window.removeEventListener('scroll', throttled.run)
throttled.cancel()
}
}, [throttled])
return <div>Scroll position: {scrollY}px</div>
}All hooks are optimized for production use:
| Hook | Optimization |
|---|---|
| useContainerSize | RAF batching, sub-pixel deduplication, ResizeObserver |
| useDragResize | RAF batching, pointer capture, event listener cleanup |
| useThrottle | Leading/trailing edge control, automatic cleanup |
| useToast | Global state pattern, listener-based updates |
- Chrome/Edge 88+
- Firefox 86+
- Safari 14.1+
- Modern mobile browsers
Required APIs:
- ResizeObserver
- Pointer Events
- RequestAnimationFrame
| Hook | Size (minified) | Size (gzipped) |
|---|---|---|
| useContainerSize | ~3.5 KB | ~1.4 KB |
| useDragResize | ~4.2 KB | ~1.7 KB |
| useThrottle | ~2.8 KB | ~1.1 KB |
| useToast | ~3.4 KB | ~1.3 KB |
| Total | ~14 KB | ~5.5 KB |
Contributions are welcome! Please ensure:
- โ
All tests pass (
npm test) - โ
ESLint passes (
npm run lint) - โ Test coverage remains above 95%
- โ
TypeScript compilation succeeds (
npm run build) - โ Documentation is updated
MIT License - feel free to use in your projects!
Created and maintained by IanF
Extracted from production applications to demonstrate:
- Advanced React patterns
- TypeScript best practices
- Proper cleanup and memory management
- Performance optimization techniques
- Comprehensive testing strategies