tom/use-event-listener

This commit is contained in:
Tom Moor
2022-04-17 11:00:28 -07:00
parent e4e98286f4
commit 2fb0182e16
7 changed files with 99 additions and 68 deletions

View File

@@ -9,6 +9,7 @@ import { depths } from "@shared/styles";
import Button from "~/components/Button";
import Fade from "~/components/Fade";
import Flex from "~/components/Flex";
import useEventListener from "~/hooks/useEventListener";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
import { supportsPassiveListener } from "~/utils/browser";
@@ -29,19 +30,17 @@ function Header({ breadcrumb, title, actions, hasSidebar }: Props) {
const passThrough = !actions && !breadcrumb && !title;
const [isScrolled, setScrolled] = React.useState(false);
const handleScroll = React.useCallback(
throttle(() => setScrolled(window.scrollY > 75), 50),
const handleScroll = React.useMemo(
() => throttle(() => setScrolled(window.scrollY > 75), 50),
[]
);
React.useEffect(() => {
window.addEventListener(
"scroll",
handleScroll,
supportsPassiveListener ? { passive: true } : false
);
return () => window.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
useEventListener(
"scroll",
handleScroll,
window,
supportsPassiveListener ? { passive: true } : { capture: false }
);
const handleClickTitle = React.useCallback(() => {
window.scrollTo({

View File

@@ -6,6 +6,7 @@ import { Portal } from "react-portal";
import styled from "styled-components";
import { depths } from "@shared/styles";
import useComponentSize from "~/hooks/useComponentSize";
import useEventListener from "~/hooks/useEventListener";
import useMediaQuery from "~/hooks/useMediaQuery";
import useViewportHeight from "~/hooks/useViewportHeight";
@@ -164,25 +165,15 @@ const FloatingToolbar = React.forwardRef(
props,
});
React.useEffect(() => {
const handleMouseDown = () => {
if (!props.active) {
setSelectingText(true);
}
};
useEventListener("mouseup", () => {
setSelectingText(false);
});
const handleMouseUp = () => {
setSelectingText(false);
};
window.addEventListener("mousedown", handleMouseDown);
window.addEventListener("mouseup", handleMouseUp);
return () => {
window.removeEventListener("mousedown", handleMouseDown);
window.removeEventListener("mouseup", handleMouseUp);
};
}, [props.active]);
useEventListener("mousedown", () => {
if (!props.active) {
setSelectingText(true);
}
});
return (
<Portal>

View File

@@ -0,0 +1,38 @@
import * as React from "react";
/**
* Helper to remove plumbing involved with adding and removing an event listener
* in components.
*
* @param eventName The name of the event to listen to.
* @param handler The handler to call when the event is triggered.
* @param element The element to attach the event listener to.
* @param options The options to pass to the event listener.
*/
export default function useEventListener<T extends EventListener>(
eventName: string,
handler: T,
element: Window | Node = window,
options: AddEventListenerOptions = {}
) {
const savedHandler = React.useRef<T>();
const { capture, passive, once } = options;
React.useEffect(() => {
savedHandler.current = handler;
}, [handler]);
React.useEffect(() => {
const isSupported = element && element.addEventListener;
if (!isSupported) {
return;
}
const eventListener: EventListener = (event) =>
savedHandler.current?.(event);
const opts = { capture, passive, once };
element.addEventListener(eventName, eventListener, opts);
return () => element.removeEventListener(eventName, eventListener, opts);
}, [eventName, element, capture, passive, once]);
}

View File

@@ -1,5 +1,6 @@
import { throttle } from "lodash";
import * as React from "react";
import useEventListener from "./useEventListener";
/**
* Mouse position as a tuple of [x, y]
@@ -17,15 +18,15 @@ export const useMousePosition = () => {
0,
]);
const updateMousePosition = throttle((ev: MouseEvent) => {
setMousePosition([ev.clientX, ev.clientY]);
}, 200);
const updateMousePosition = React.useMemo(
() =>
throttle((ev: MouseEvent) => {
setMousePosition([ev.clientX, ev.clientY]);
}, 200),
[]
);
React.useEffect(() => {
window.addEventListener("mousemove", updateMousePosition);
return () => window.removeEventListener("mousemove", updateMousePosition);
}, []);
useEventListener("mousemove", updateMousePosition);
return mousePosition;
};

View File

@@ -1,4 +1,5 @@
import * as React from "react";
import useEventListener from "./useEventListener";
/**
* Hook to return page visibility state.
@@ -8,13 +9,11 @@ import * as React from "react";
export default function usePageVisibility(): boolean {
const [visible, setVisible] = React.useState(true);
React.useEffect(() => {
const handleVisibilityChange = () => setVisible(!document.hidden);
useEventListener(
"visibilitychange",
() => setVisible(!document.hidden),
document
);
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, []);
return visible;
}

View File

@@ -1,6 +1,7 @@
import * as React from "react";
import { Primitive } from "utility-types";
import Storage from "~/utils/Storage";
import useEventListener from "./useEventListener";
/**
* A hook with the same API as `useState` that persists its value locally and
@@ -36,16 +37,11 @@ export default function usePersistedState(
};
// Listen to the key changing in other tabs so we can keep UI in sync
React.useEffect(() => {
const updateValue = (event: any) => {
if (event.key === key && event.newValue) {
setStoredValue(JSON.parse(event.newValue));
}
};
window.addEventListener("storage", updateValue);
return () => window.removeEventListener("storage", updateValue);
}, [key]);
useEventListener("storage", (event: StorageEvent) => {
if (event.key === key && event.newValue) {
setStoredValue(JSON.parse(event.newValue));
}
});
return [storedValue, setValue];
}

View File

@@ -1,28 +1,35 @@
import { debounce } from "lodash";
import * as React from "react";
import useEventListener from "./useEventListener";
/**
* A debounced hook that listens to the window resize event and returns the
* size of the current window.
*
* @returns An object containing width and height of the current window
*/
export default function useWindowSize() {
const [windowSize, setWindowSize] = React.useState({
width: window.innerWidth,
height: window.innerHeight,
});
React.useEffect(() => {
// Handler to call on window resize
const handleResize = debounce(() => {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}, 100);
const handleResize = React.useMemo(
() =>
debounce(() => {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}, 100),
[]
);
// Add event listener
window.addEventListener("resize", handleResize);
useEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Call handler right away so state gets updated with initial window size
handleResize();
return () => window.removeEventListener("resize", handleResize);
}, []);
return windowSize;
}