Skip to content

Instantly share code, notes, and snippets.

@gragland
Last active January 21, 2022 13:02
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gragland/4e3d9b1c934a18dc76f585350f97e321 to your computer and use it in GitHub Desktop.
Save gragland/4e3d9b1c934a18dc76f585350f97e321 to your computer and use it in GitHub Desktop.
import { useState, useEffect } from 'react';
// Usage
function App() {
const size = useWindowSize();
return (
<div>
{size.width}px / {size.height}px
</div>
);
}
// Hook
function useWindowSize() {
// Initialize state with undefined width/height so server and client renders match
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount
return windowSize;
}
@olee
Copy link

olee commented Dec 30, 2018

This version uses an array as result (because it's just better for destructuring) and has more options with smaller footprint.

import { useState, useEffect } from 'react';

type Result = [number | undefined, number | undefined];

/**
 * Returns current window size and updates state if window if resized
 * @param  element            Optional element to use for accessing the window object instead of global window variable
 * @param  disableResizeEvent Disable the resize event and only get initial window size
 * @return [width, height]
 */
export default function useWindowSize(element?: HTMLElement | null, disableResizeEvent?: boolean): Result {
    // If we are not even in a browser environment, do not even useEffect - window size will always be undefined
    if (typeof window !== 'object')
        return [undefined, undefined];
    // Get reference to window objecz
    const wnd = element && element.ownerDocument && element.ownerDocument.defaultView || window;
    // Get / initialize state
    const [result, setSize] = useState<Result>([wnd && wnd.innerWidth, wnd && wnd.innerHeight]);
    useEffect(() => {
        if (typeof wnd === 'object') {
            const handleResize = () => setSize([wnd.innerWidth, wnd.innerHeight]);
            handleResize();
            if (!disableResizeEvent) {
                wnd.addEventListener('resize', handleResize);
                return () => wnd.removeEventListener('resize', handleResize);
            }
        }
    }, [wnd, disableResizeEvent]);
    return result;
}

@antifriz
Copy link

When using typescript and @types/react:16.9.2 effect callback is defined as follows:

type EffectCallback = () => (void | (() => void | undefined))

this leads to returning false being treated as invalid return type.
I'd suggest to replace it just with return; or return () => {} not to be bothered with inconsistent return points.

@alfred-pb
Copy link

Any reason why you're not debouncing or throttling this hook?

@mmiocevic
Copy link

mmiocevic commented Nov 4, 2019

Any reason why you're not debouncing or throttling this hook?

Here you go:

  import { useEffect, useState } from 'react';

  const debounce = (delay: number, fn: any) => {
     let timerId: any;

     return function (...args: any[]) {
        if (timerId) {
           clearTimeout(timerId);
        }

        timerId = setTimeout(() => {
           fn(...args);
           timerId = null;
        }, delay);
     };
  };

  export const useWindowSize = (debounceTime?: number) => {
     const isClient: boolean = typeof window === 'object';
     const getSize = () => ({
        width: isClient ? window.innerWidth : undefined,
        height: isClient ? window.innerHeight : undefined
     });
     const [windowSize, setWindowSize] = useState(getSize);

     useEffect(() => {
        if (!isClient) {
           return;
        }

        const handleResize = () => {
           setWindowSize(getSize());
        };

        let handleResizeFn = handleResize;

        if (debounceTime) {
           handleResizeFn = debounce(debounceTime, handleResize);
        }

        window.addEventListener('resize', handleResizeFn);

        return () => window.removeEventListener('resize', handleResizeFn);
     }, []);

     return windowSize;
  };

@dileepthomas
Copy link

dileepthomas commented Dec 5, 2019

Thanks for the reusable hook, it helped me a lot. As going head i thought of writing the test case for the useWindowSize hook.

so i was able to cover 90% but since i was also new to unit testing. anyone would have a fair knowledge how can we have 100% code coverage.

so the people won't need to write it again and again for the test case.

Also please suggest any better way, i was using enzyme and jest but since hooks support was less i was using react testing library.

@testing-library/react
@testing-library/react-hooks

so using act how can i make the window to a different data type rather than object so it will have 100% test coverage.

//hooks.js

import { useState, useEffect } from "react";

function useWindowSize() {
    const isClient = typeof window === "object";

    function getSize() {
        return {
            width: isClient ? window.innerWidth : undefined,
            height: isClient ? window.innerHeight : undefined,
        };
    }

    const [windowSize, setWindowSize] = useState(getSize);

    useEffect(() => {
        if (!isClient) {
            return false;
        }

        function handleResize() {
            setWindowSize(getSize());
        }

        window.addEventListener("resize", handleResize);
        return () => window.removeEventListener("resize", handleResize);
    }, []); // Empty array ensures that effect is only run on mount and unmount

    return windowSize;
}

export default {useWindowSize}

// hooks.test.js

import { renderHook, act } from "@testing-library/react-hooks";
import { fireEvent } from "@testing-library/react";
import hooks from "..";

const { useWindowSize } = hooks;

describe("hooks", () => {
    it("should return a new size of window", () => {
        const { result } = renderHook(() => useWindowSize());
        expect(result.current.width).toBe(1024);
        expect(result.current.height).toBe(768);

        act(() => {
            // change the viewport to 500px.
            window.innerWidth = 500;
            window.innerHeight = 500;
            // trigger the window resize event
            fireEvent(window, new Event("resize"));
        });

        expect(result.current.width).toBe(500);
        expect(result.current.height).toBe(500);
    });

    // how can i test if window is not an typeof object
    it("should exit if its not window", () => {
        const { result } = renderHook(() => useWindowSize());
        act(() => {
            // change the window from object 
            window = "";
            fireEvent(window, new Event("resize"));
        });
    });
});

@HariAbinesh
Copy link

When I use this hook twice, it adds Event Listener to window twice. Should this be adding only once and Event caching needed ?

@jelizarovas
Copy link

react_devtools_backend.js:6 ./src/utils/useWindowSize.js
  Line 27:6:  React Hook useEffect has missing dependencies: 'getSize' and 'isClient'. Either include them or remove the dependency array  react-hooks/exhaustive-deps

@lax4mike
Copy link

lax4mike commented Jul 7, 2020

Hello! 👋🏻

This code has an issue with server rendering. I ran in to a similar issue here: gatsbyjs/gatsby#14601

The underlying issue is in getSize

return {
  width: isClient ? window.innerWidth : undefined,
  height: isClient ? window.innerHeight : undefined
};

The problem is that the server render value of windowSize and the first client render won't match. (eg, width on the server will be undefined, client will be window.innerWidth). This confuses React and you will see incorrect values on first load, as described in the github issue above and demonstrated here on a large screen: http://mikelambert.me/gatsby-bug/

To fix this, I modified the hook to always initialize the values to undefined (so the server render and first client render match) and to update the values after the first render on the client.

export default function useWindowSize() {
  // initialize to undefined so the server render and first client render match
  // https://github.com/gatsbyjs/gatsby/issues/14601 
  const [windowSize, setWindowSize] = useState({
    width: undefined,
    height: undefined,
  });

  useEffect(() => {
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }

    window.addEventListener("resize", handleResize);
    handleResize();

    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return windowSize;
}

@gragland
Copy link
Author

gragland commented Jul 30, 2020

@lax4mike thanks for pointing out the SSR issue! I've updated this gist and the post.

@ivanjeremic
Copy link

ivanjeremic commented Dec 5, 2021

change the useEffect to useLayoutEffect because you see a flicker before it's set in some cases. useLayoutEffect is definitely the way to go here. Here an example where a div is centered based on the body, if you change it to a normal useEffect you will see it flicker. https://codesandbox.io/s/still-wave-q0pk7

@mimiqkz
Copy link

mimiqkz commented Jan 21, 2022

Long time has passed , why haven't this been updated with debounce or throttle ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment