Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/__testing__/useWindowDimensions.client.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { act, renderHook } from '@testing-library/react';
import { useWindowDimensions } from '../custom/Helpers/Dimension/windowSize';

describe('useWindowDimensions (client)', () => {
it('reads the real window dimensions after mount', () => {
const { result } = renderHook(() => useWindowDimensions());
// The mount effect syncs state to the live window size (jsdom default).
expect(result.current).toEqual({
width: window.innerWidth,
height: window.innerHeight
});
});

it('updates (debounced) on window resize', () => {
jest.useFakeTimers();
try {
const { result } = renderHook(() => useWindowDimensions());

act(() => {
(window as unknown as { innerWidth: number }).innerWidth = 480;
(window as unknown as { innerHeight: number }).innerHeight = 640;
window.dispatchEvent(new Event('resize'));
// Resize handling is debounced by 500ms.
jest.advanceTimersByTime(500);
});

expect(result.current).toEqual({ width: 480, height: 640 });
} finally {
jest.useRealTimers();
}
});
});
26 changes: 26 additions & 0 deletions src/__testing__/useWindowDimensions.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @jest-environment node
*/
import React from 'react';
import { renderToString } from 'react-dom/server';
import { useWindowDimensions } from '../custom/Helpers/Dimension/windowSize';

function Probe() {
const { width, height } = useWindowDimensions();
return React.createElement('div', null, `${width}x${height}`);
}

describe('useWindowDimensions SSR safety', () => {
it('does not read window during render and falls back to 0x0', () => {
// The `node` environment has no `window`, mirroring Node SSR and
// Next.js static-export prerender. Initial state is zeroed and the
// mount effect does not run during renderToString, so `window` is
// never touched during render.
expect(typeof window).toBe('undefined');
let html = '';
expect(() => {
html = renderToString(React.createElement(Probe));
}).not.toThrow();
expect(html).toContain('0x0');
});
});
25 changes: 23 additions & 2 deletions src/custom/Helpers/Dimension/windowSize.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import React from 'react';

/**
* Returns the width and height of the window.
* Reads the current window dimensions.
*
* Only invoked on the client - from the mount effect and the resize handler
* in `useWindowDimensions`, never during render. The `typeof window` guard is
* a defensive net so a stray render-time call can never throw a
* `ReferenceError` under SSR / static prerender.
*
* @returns {WindowDimensions} { width, height }
*/
function getWindowDimensions(): WindowDimensions {
if (typeof window === 'undefined') {
return { width: 0, height: 0 };
}
Comment thread
leecalcote marked this conversation as resolved.
const { innerWidth: width, innerHeight: height } = window;
return {
width,
Expand All @@ -16,12 +24,25 @@ function getWindowDimensions(): WindowDimensions {
/**
* Custom hook for getting window dimensions.
*
* State is initialised to zeroed dimensions rather than by reading `window`
* in the `useState` initialiser. This keeps the server render and the first
* client (hydration) render identical - reading `window` during render would
* make them diverge (`0x0` on the server vs the real size on the client) and
* trigger a hydration mismatch. The real dimensions are read once on mount
* via the effect below and then kept current by the debounced resize listener.
*
* @returns {WindowDimensions} { width, height }
*/
export function useWindowDimensions(): WindowDimensions {
const [windowDimensions, setWindowDimensions] = React.useState(getWindowDimensions());
const [windowDimensions, setWindowDimensions] = React.useState<WindowDimensions>({
width: 0,
height: 0
});

React.useEffect(() => {
// Sync to the real dimensions on mount (client only).
setWindowDimensions(getWindowDimensions());

let resizeTimeout: number;

function handleResize() {
Expand Down
Loading