Skip to content

Commit f5a856f

Browse files
authored
Merge pull request #1569 from layer5io/fix/window-dimensions-ssr-guard
fix(useWindowDimensions): guard window access for SSR / static prerender
2 parents 61f68df + 89dbc88 commit f5a856f

3 files changed

Lines changed: 81 additions & 2 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
import { useWindowDimensions } from '../custom/Helpers/Dimension/windowSize';
3+
4+
describe('useWindowDimensions (client)', () => {
5+
it('reads the real window dimensions after mount', () => {
6+
const { result } = renderHook(() => useWindowDimensions());
7+
// The mount effect syncs state to the live window size (jsdom default).
8+
expect(result.current).toEqual({
9+
width: window.innerWidth,
10+
height: window.innerHeight
11+
});
12+
});
13+
14+
it('updates (debounced) on window resize', () => {
15+
jest.useFakeTimers();
16+
try {
17+
const { result } = renderHook(() => useWindowDimensions());
18+
19+
act(() => {
20+
(window as unknown as { innerWidth: number }).innerWidth = 480;
21+
(window as unknown as { innerHeight: number }).innerHeight = 640;
22+
window.dispatchEvent(new Event('resize'));
23+
// Resize handling is debounced by 500ms.
24+
jest.advanceTimersByTime(500);
25+
});
26+
27+
expect(result.current).toEqual({ width: 480, height: 640 });
28+
} finally {
29+
jest.useRealTimers();
30+
}
31+
});
32+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
import React from 'react';
5+
import { renderToString } from 'react-dom/server';
6+
import { useWindowDimensions } from '../custom/Helpers/Dimension/windowSize';
7+
8+
function Probe() {
9+
const { width, height } = useWindowDimensions();
10+
return React.createElement('div', null, `${width}x${height}`);
11+
}
12+
13+
describe('useWindowDimensions SSR safety', () => {
14+
it('does not read window during render and falls back to 0x0', () => {
15+
// The `node` environment has no `window`, mirroring Node SSR and
16+
// Next.js static-export prerender. Initial state is zeroed and the
17+
// mount effect does not run during renderToString, so `window` is
18+
// never touched during render.
19+
expect(typeof window).toBe('undefined');
20+
let html = '';
21+
expect(() => {
22+
html = renderToString(React.createElement(Probe));
23+
}).not.toThrow();
24+
expect(html).toContain('0x0');
25+
});
26+
});

src/custom/Helpers/Dimension/windowSize.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import React from 'react';
22

33
/**
4-
* Returns the width and height of the window.
4+
* Reads the current window dimensions.
5+
*
6+
* Only invoked on the client - from the mount effect and the resize handler
7+
* in `useWindowDimensions`, never during render. The `typeof window` guard is
8+
* a defensive net so a stray render-time call can never throw a
9+
* `ReferenceError` under SSR / static prerender.
510
*
611
* @returns {WindowDimensions} { width, height }
712
*/
813
function getWindowDimensions(): WindowDimensions {
14+
if (typeof window === 'undefined') {
15+
return { width: 0, height: 0 };
16+
}
917
const { innerWidth: width, innerHeight: height } = window;
1018
return {
1119
width,
@@ -16,12 +24,25 @@ function getWindowDimensions(): WindowDimensions {
1624
/**
1725
* Custom hook for getting window dimensions.
1826
*
27+
* State is initialised to zeroed dimensions rather than by reading `window`
28+
* in the `useState` initialiser. This keeps the server render and the first
29+
* client (hydration) render identical - reading `window` during render would
30+
* make them diverge (`0x0` on the server vs the real size on the client) and
31+
* trigger a hydration mismatch. The real dimensions are read once on mount
32+
* via the effect below and then kept current by the debounced resize listener.
33+
*
1934
* @returns {WindowDimensions} { width, height }
2035
*/
2136
export function useWindowDimensions(): WindowDimensions {
22-
const [windowDimensions, setWindowDimensions] = React.useState(getWindowDimensions());
37+
const [windowDimensions, setWindowDimensions] = React.useState<WindowDimensions>({
38+
width: 0,
39+
height: 0
40+
});
2341

2442
React.useEffect(() => {
43+
// Sync to the real dimensions on mount (client only).
44+
setWindowDimensions(getWindowDimensions());
45+
2546
let resizeTimeout: number;
2647

2748
function handleResize() {

0 commit comments

Comments
 (0)