Skip to content

Repository files navigation

React Hooks Collection

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.

TypeScript Test Coverage Tests License: MIT

โœจ Features

  • ๐Ÿš€ 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

๐Ÿ“ฆ Hooks Included

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

๐Ÿš€ Quick Start

Installation

npm install react-hooks-collection

Basic Usage

import { 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>
  )
}

๐Ÿ“– Detailed Documentation

1. useContainerSize

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

2. useDragResize

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
})

3. useThrottle

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>
}

4. useToast

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])} />
}

๐Ÿงช Testing

All hooks include comprehensive test suites with 98.25% coverage.

Running Tests

# Run all unit tests
npm test

# Run tests in watch mode
npm run test:watch

# Generate coverage report
npm run test:coverage

Example Test

import { 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')
})

๐Ÿ“– Storybook

Interactive documentation and examples for all hooks are available in Storybook.

Running Storybook

# Start Storybook development server
npm run storybook

# Build static Storybook
npm run build-storybook

Storybook 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

Available Stories

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

๐ŸŽญ E2E Testing with Playwright

End-to-end tests verify hook functionality through real browser interactions.

Running E2E Tests

# 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:headed

Test Coverage

E2E 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

Example E2E Test

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


๐ŸŽฏ Advanced Patterns

Hook Composition

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
}

Responsive Breakpoints

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>
  )
}

Throttled Scroll Handler

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>
}

โšก Performance

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

๐ŸŒ Browser Support

  • Chrome/Edge 88+
  • Firefox 86+
  • Safari 14.1+
  • Modern mobile browsers

Required APIs:

  • ResizeObserver
  • Pointer Events
  • RequestAnimationFrame

๐Ÿ“ฆ Bundle Size

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

๐Ÿค Contributing

Contributions are welcome! Please ensure:

  1. โœ… All tests pass (npm test)
  2. โœ… ESLint passes (npm run lint)
  3. โœ… Test coverage remains above 95%
  4. โœ… TypeScript compilation succeeds (npm run build)
  5. โœ… Documentation is updated

๐Ÿ“„ License

MIT License - feel free to use in your projects!


๐Ÿ™ Credits

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

๐Ÿ“š Further Reading

About

Production-grade React hooks collection: drag-resize, throttle, toast notifications, and container size tracking

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages