UNPKG

@winglet/react-utils

Version:

React utility library providing custom hooks, higher-order components (HOCs), and utility functions to enhance React application development with improved reusability and functionality

294 lines (293 loc) 8.86 kB
/** * Tracks browser window dimensions with automatic updates on resize events. * * This hook subscribes to window resize events and provides real-time viewport dimensions, * enabling responsive components that adapt to window size changes. It handles event listener * cleanup automatically and provides initial size measurement after component mount. * * ### Use Cases * - **Responsive Components**: Adapt layout based on current viewport size * - **Conditional Rendering**: Show/hide elements based on available screen space * - **Dynamic Calculations**: Calculate dimensions relative to viewport (e.g., modals, overlays) * - **Breakpoint Logic**: Implement custom responsive breakpoints in JavaScript * - **Performance Optimization**: Render different components for mobile vs desktop * - **Accessibility**: Adjust UI density and touch targets based on screen size * * ### Performance Considerations * - **High Frequency Events**: Resize events fire frequently during window resizing * - **Debouncing Recommended**: For expensive computations, combine with `useDebounce` * - **SSR Compatibility**: Returns `{width: 0, height: 0}` on server-side * - **Memory Efficiency**: Automatically cleans up event listeners on unmount * * ### Responsive Design Patterns * Use this hook to implement responsive behavior that CSS media queries can't handle: * - Dynamic grid columns based on available width * - Conditional component rendering * - Viewport-aware animations and transitions * - Content sizing relative to viewport * * @example * ```typescript * // Basic responsive layout switching * const ResponsiveLayout = () => { * const { width, height } = useWindowSize(); * * const isMobile = width < 768; * const isTablet = width >= 768 && width < 1024; * const isDesktop = width >= 1024; * * // Content area sized to viewport * return ( * <div className={`layout ${isMobile ? 'mobile' : isTablet ? 'tablet' : 'desktop'}`}> * {isMobile ? ( * <MobileNavigation /> * ) : ( * <DesktopNavigation /> * )} * * <main style={{ minHeight: height - 80 }}> * <Content /> * </main> * </div> * ); * }; * * // Custom breakpoint hook composition * const useBreakpoint = () => { * const { width } = useWindowSize(); * * return useMemo(() => { * if (width < 480) return 'xs'; * if (width < 768) return 'sm'; * if (width < 1024) return 'md'; * if (width < 1280) return 'lg'; * if (width < 1536) return 'xl'; * return '2xl'; * }, [width]); * }; * * const AdaptiveComponent = () => { * const breakpoint = useBreakpoint(); * * const config = useMemo(() => { * switch (breakpoint) { * case 'xs': return { columns: 1, cardSize: 'small', showSidebar: false }; * case 'sm': return { columns: 2, cardSize: 'small', showSidebar: false }; * case 'md': return { columns: 2, cardSize: 'medium', showSidebar: true }; * case 'lg': return { columns: 3, cardSize: 'medium', showSidebar: true }; * default: return { columns: 4, cardSize: 'large', showSidebar: true }; * } * }, [breakpoint]); * * return ( * <div className={`responsive-grid grid-${config.columns}`}> * // Component adapts to breakpoint * </div> * ); * }; * * // Dynamic grid with calculated columns * const ResponsiveGrid = ({ items, minItemWidth = 250 }) => { * const { width } = useWindowSize(); * * const columns = Math.max(1, Math.floor(width / minItemWidth)); * const itemWidth = Math.floor((width - (columns + 1) * 20) / columns); * * return ( * <div * style={{ * display: 'grid', * gridTemplateColumns: `repeat(${columns}, 1fr)`, * gap: '20px', * padding: '20px' * }} * > * {items.map((item, index) => ( * <GridItem * key={item.id} * data={item} * width={itemWidth} * /> * ))} * </div> * ); * }; * * // Viewport-aware modal positioning * const AdaptiveModal = ({ isOpen, onClose, children }) => { * const { width, height } = useWindowSize(); * * const modalStyle = useMemo(() => { * const isMobile = width < 768; * * if (isMobile) { * // Full-screen on mobile * return { * position: 'fixed', * top: 0, * left: 0, * width: '100%', * height: '100%', * transform: 'none' * }; * } else { * // Centered with max dimensions on desktop * const maxWidth = Math.min(800, width * 0.9); * const maxHeight = Math.min(600, height * 0.8); * * return { * position: 'fixed', * top: '50%', * left: '50%', * transform: 'translate(-50%, -50%)', * width: maxWidth, * height: maxHeight, * maxWidth: '90vw', * maxHeight: '90vh' * }; * } * }, [width, height]); * * if (!isOpen) return null; * * return ( * <> * <div className="modal-backdrop" onClick={onClose} /> * <div className="modal" style={modalStyle}> * {children} * </div> * </> * ); * }; * * // Performance-optimized with debouncing * const DeboucedResponsiveChart = ({ data }) => { * const windowSize = useWindowSize(); * * // Debounce resize events to prevent excessive chart re-renders * const debouncedSize = useDebounce(() => windowSize, 250); * * const chartDimensions = useMemo(() => { * const { width, height } = debouncedSize; * const chartWidth = Math.min(width - 40, 1200); * const chartHeight = Math.min(height * 0.6, 400); * * return { width: chartWidth, height: chartHeight }; * }, [debouncedSize]); * * return ( * <div className="chart-container"> * <Chart * data={data} * width={chartDimensions.width} * height={chartDimensions.height} * /> * </div> * ); * }; * * // Orientation-aware component * const OrientationSensitive = () => { * const { width, height } = useWindowSize(); * * const orientation = width > height ? 'landscape' : 'portrait'; * const aspectRatio = (width / height).toFixed(2); * * return ( * <div className={`app-${orientation}`}> * <div className="viewport-info"> * <span>Size: {width} × {height}</span> * <span>Orientation: {orientation}</span> * <span>Aspect Ratio: {aspectRatio}</span> * </div> * * {orientation === 'landscape' ? ( * <LandscapeLayout /> * ) : ( * <PortraitLayout /> * )} * </div> * ); * }; * * // Fullscreen component with viewport sizing * const FullscreenCanvas = () => { * const { width, height } = useWindowSize(); * const canvasRef = useRef<HTMLCanvasElement>(null); * * useEffect(() => { * const canvas = canvasRef.current; * if (!canvas) return; * * // Update canvas size to match viewport * canvas.width = width; * canvas.height = height; * * // Update canvas rendering based on new size * const ctx = canvas.getContext('2d'); * if (ctx) { * redrawCanvas(ctx, width, height); * } * }, [width, height]); * * return ( * <canvas * ref={canvasRef} * style={{ * position: 'fixed', * top: 0, * left: 0, * width: '100vw', * height: '100vh' * }} * /> * ); * }; * * // Responsive text sizing * const ScalingText = ({ children }) => { * const { width } = useWindowSize(); * * const fontSize = useMemo(() => { * // Scale font size based on viewport width * const baseSize = 16; * const scaleFactor = Math.max(0.8, Math.min(1.5, width / 1200)); * return baseSize * scaleFactor; * }, [width]); * * return ( * <div style={{ fontSize: `${fontSize}px` }}> * {children} * </div> * ); * }; * * // Conditional loading based on screen size * const ConditionalFeatures = () => { * const { width } = useWindowSize(); * const isLargeScreen = width >= 1200; * * return ( * <div> * <MainContent /> * // Only load heavy components on large screens * {isLargeScreen && ( * <Suspense fallback={<div>Loading advanced features...</div>}> * <AdvancedAnalytics /> * <RealTimeChart /> * <DetailedControls /> * </Suspense> * )} * </div> * ); * }; * ``` * * @returns An object containing the current window dimensions: * - width: The inner width of the window in pixels (0 during SSR) * - height: The inner height of the window in pixels (0 during SSR) */ export declare const useWindowSize: () => { width: number; height: number; };