Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
import { useState, useEffect } from 'react';
// Usage
function App() {
const size = useWindowSize();
return (
<div>
{size.width}px / {size.height}px
</div>
);
}
// Hook
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;
}
@mattfysh

This comment has been minimized.

Copy link

mattfysh commented Oct 31, 2018

You could pass a function into useState, so the initial state is calculated only once (and not on each render), i.e:

const [windowSize, setWindowSize] = useState(getSize)
@gragland

This comment has been minimized.

Copy link
Owner Author

gragland commented Oct 31, 2018

@mattfysh Good catch! Will update shortly.

@AndyBarron

This comment has been minimized.

Copy link

AndyBarron commented Oct 31, 2018

Couldn't we declare isClient and getSize in the module scope so that they're only checked/allocated once? I would also be a fan of returning [width, height] as a tuple, but that's super subjective.

Also, the useEffect callback unconditionally accesses window, so the isClient check wouldn't prevent you from crashing in a non-client environment.

import { useState, useEffect } from 'react';

// Usage
function App() {
  const [width, height] = useWindowSize();

  return (
    <div>
      {width}px / {height}px
    </div>
  );
}

// Helpers
const IS_CLIENT = typeof window === 'object';
const getSize = IS_CLIENT ?
  () => [window.innerWidth, window.innerHeight] :
  () => [undefined, undefined];

// Hook
function useWindowSize() {
  const [windowSize, setWindowSize] = useState(getSize);

  useEffect(() => {
    if (!IS_CLIENT) {
      return;
    }
    
    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;
}
@gragland

This comment has been minimized.

Copy link
Owner Author

gragland commented Nov 2, 2018

@AndyBarron Thanks, fixed the issue with isClient not being checked within useEffect. And agree that isClient and getSize should probably be in module scope. For the time being, I'm trying to keep the usage and hook functions fairly self contained so it's easier to copy one or the other into a project without missing anything.. but I'll likely resolve that soon by breaking the two up into separate code blocks, in which case I'll then move stuff out into module scope where it makes sense. Really appreciate the detailed feedback you've been giving on all the hooks!

@mattfysh

This comment has been minimized.

Copy link

mattfysh commented Nov 5, 2018

Also, the useEffect callback unconditionally accesses window, so the isClient check wouldn't prevent you from crashing in a non-client environment.

Given that useEffect somewhat relates to componentDidMount and componentDidUpdate, does this mean they are not executed in server-side rendering contexts? My hunch is that they aren't invoked, and the access of window should be okay?

@ianobermiller

This comment has been minimized.

Copy link

ianobermiller commented Nov 9, 2018

I suggest moving handleResize into useEffect as in @AndyBarron's example

@gustavoguichard

This comment has been minimized.

Copy link

gustavoguichard commented Nov 12, 2018

An optional debounce value would fit nicely here ;)

@gragland

This comment has been minimized.

Copy link
Owner Author

gragland commented Nov 20, 2018

@yazeedb

This comment has been minimized.

Copy link

yazeedb commented Nov 25, 2018

@gragland Thanks for this hook, I'm using it on a project!
May I write a brief blog post on it? Will link back to you and useHooks.com

@olee

This comment has been minimized.

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

This comment has been minimized.

Copy link

antifriz commented Sep 21, 2019

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

This comment has been minimized.

Copy link

alfred-pb commented Oct 24, 2019

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

@mmiocevic

This comment has been minimized.

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

This comment has been minimized.

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"));
        });
    });
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.