Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | 48x 48x 48x 48x 48x 48x 48x 48x 11x 11x 2x 9x 9x 9x 48x 20x 2x 18x 18x 18x 18x 18x 18x 18x 22x 2x 2x 18x 13x 3x 3x 1x 1x 3x 10x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 2x 8x 18x 18x 18x 18x 48x | import { useCallback, useEffect, useRef, useState } from "react";
import type {
OnWindowSizeChange,
UseWindowSizeOptions,
UseWindowSizeReturn,
WindowSize,
} from "./types";
import { areSizesEqual, getWindowSize, isWindowAvailable } from "./utils";
/**
* A React hook for tracking the browser window size.
*
* Features:
* - Real-time `width`/`height` updates on resize
* - Optional debounce or throttle to limit update frequency
* - SSR-safe with configurable initial size (no hydration mismatch)
* - Skips re-renders when the size hasn't actually changed
* - Optional `onChange` callback and dynamic enable/disable
* - Choose whether the scrollbar is included in the measurement
*
* @param options - Configuration options for the hook
* @returns The current window size as `{ width, height }`
*
* @example
* ```tsx
* // Basic usage
* function Component() {
* const { width, height } = useWindowSize();
* return (
* <div>
* {width} × {height}
* {width < 768 ? <MobileView /> : <DesktopView />}
* </div>
* );
* }
* ```
*
* @example
* ```tsx
* // Debounced updates (great for expensive layout work)
* const { width } = useWindowSize({ debounceMs: 200 });
* ```
*
* @example
* ```tsx
* // Throttled updates with a change callback
* const size = useWindowSize({
* throttleMs: 100,
* onChange: ({ width }) => {
* if (width < 768) closeSidebar();
* },
* });
* ```
*
* @example
* ```tsx
* // SSR-safe with initial values to avoid hydration mismatches
* const { width, height } = useWindowSize({
* initialWidth: 1024,
* initialHeight: 768,
* });
* ```
*/
export function useWindowSize(
options: UseWindowSizeOptions = {}
): UseWindowSizeReturn {
const {
initialWidth = 0,
initialHeight = 0,
debounceMs = 0,
throttleMs = 0,
includeScrollbar = true,
enabled = true,
onChange,
} = options;
const isSupported = isWindowAvailable();
// The first render MUST be deterministic and identical on server and client
// (initialWidth/initialHeight) — reading the real window size here would make
// the client's first render diverge from the server HTML and cause a
// hydration mismatch. The real size is measured in the mount effect below.
const [size, setSize] = useState<WindowSize>(() => ({
width: initialWidth,
height: initialHeight,
}));
// Keep the latest callback in a ref so it never re-creates the effect.
const onChangeRef = useRef<OnWindowSizeChange | undefined>(onChange);
onChangeRef.current = onChange;
// Mirror the latest size so updateSize can compare without being a dependency.
const sizeRef = useRef<WindowSize>(size);
sizeRef.current = size;
const updateSize = useCallback(() => {
const next = getWindowSize(includeScrollbar);
// No-op skip: bail out when the size is unchanged to avoid re-renders.
if (areSizesEqual(sizeRef.current, next)) {
return;
}
sizeRef.current = next;
setSize(next);
onChangeRef.current?.(next);
}, [includeScrollbar]);
useEffect(() => {
if (!isSupported || !enabled) {
return;
}
// Post-mount sync: adopt the real window size WITHOUT firing onChange
// (onChange is reserved for actual resize events, not the initial measure
// that replaces the SSR-safe initial value after hydration).
const mounted = getWindowSize(includeScrollbar);
Eif (!areSizesEqual(sizeRef.current, mounted)) {
sizeRef.current = mounted;
setSize(mounted);
}
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let lastRun = 0;
const clearPending = () => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
};
const handleResize = () => {
if (debounceMs > 0) {
clearPending();
timeoutId = setTimeout(() => {
timeoutId = null;
updateSize();
}, debounceMs);
return;
}
if (throttleMs > 0) {
const now = Date.now();
const remaining = throttleMs - (now - lastRun);
if (remaining <= 0) {
lastRun = now;
updateSize();
} else {
clearPending();
timeoutId = setTimeout(() => {
timeoutId = null;
lastRun = Date.now();
updateSize();
}, remaining);
}
return;
}
updateSize();
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
clearPending();
};
}, [isSupported, enabled, debounceMs, throttleMs, updateSize]);
return size;
}
|