-
-
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; | |
} |
@mattfysh Good catch! Will update shortly.
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;
}
@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!
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?
I suggest moving handleResize
into useEffect
as in @andybarron's example
An optional debounce value would fit nicely here ;)
@ianobermiller done!
@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
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;
}
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.
Any reason why you're not debouncing or throttling this hook?
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;
};
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"));
});
});
});
When I use this hook twice, it adds Event Listener to window twice. Should this be adding only once and Event caching needed ?
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
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;
}
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
Long time has passed , why haven't this been updated with debounce or throttle ?
You could pass a function into
useState
, so the initial state is calculated only once (and not on each render), i.e: