Add 'useSetTimeout' hook to manage timers (#2134)

This commit is contained in:
Jon Koops 2022-02-23 11:29:50 +01:00 committed by GitHub
parent 79827249ed
commit 06de428848
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 97 additions and 0 deletions

View file

@ -4,6 +4,7 @@ import { AlertVariant } from "@patternfly/react-core";
import type { AxiosError } from "axios";
import useRequiredContext from "../../utils/useRequiredContext";
import useSetTimeout from "../../utils/useSetTimeout";
import { AlertPanel, AlertType } from "./AlertPanel";
type AlertProps = {
@ -23,6 +24,7 @@ export const useAlerts = () => useRequiredContext(AlertContext);
export const AlertProvider: FunctionComponent = ({ children }) => {
const { t } = useTranslation();
const [alerts, setAlerts] = useState<AlertType[]>([]);
const setTimeout = useSetTimeout();
const createId = () => new Date().getTime();

View file

@ -0,0 +1,61 @@
/**
* @jest-environment jsdom
*/
import { renderHook } from "@testing-library/react-hooks";
import useSetTimeout from "./useSetTimeout";
jest.useFakeTimers();
describe("useSetTimeout", () => {
it("schedules timeouts and triggers the callbacks", () => {
const { result } = renderHook(() => useSetTimeout());
const setTimeoutSpy = jest.spyOn(global, "setTimeout");
// Schedule some timeouts...
const callback1 = jest.fn();
const callback2 = jest.fn();
result.current(callback1, 1000);
result.current(callback2, 500);
// Ensure that setTimeout was actually called with the correct arguments.
expect(setTimeoutSpy).toHaveBeenCalledTimes(2);
expect(setTimeoutSpy).toBeCalledWith(expect.any(Function), 1000);
expect(setTimeoutSpy).toBeCalledWith(expect.any(Function), 500);
// Ensure callbacks are called after timers run.
expect(callback2).not.toBeCalled();
jest.advanceTimersByTime(500);
expect(callback1).not.toBeCalled();
expect(callback2).toBeCalled();
jest.advanceTimersByTime(500);
expect(callback1).toBeCalled();
setTimeoutSpy.mockRestore();
});
it("throws if a timeout is scheduled after the component has unmounted", () => {
const { result, unmount } = renderHook(() => useSetTimeout());
unmount();
expect(() => result.current(jest.fn(), 1000)).toThrowError(
"Can't schedule a timeout on an unmounted component."
);
});
it("clears a timeout if the component unmounts", () => {
const { result, unmount } = renderHook(() => useSetTimeout());
const timerId = 42;
const setTimeoutSpy = jest
.spyOn(global, "setTimeout")
.mockReturnValueOnce(timerId as unknown as NodeJS.Timeout);
const clearTimeoutSpy = jest.spyOn(global, "clearTimeout");
result.current(jest.fn(), 1000);
unmount();
expect(clearTimeoutSpy).toBeCalledWith(timerId);
setTimeoutSpy.mockRestore();
});
});

View file

@ -0,0 +1,34 @@
import { useEffect, useRef } from "react";
export default function useSetTimeout() {
const didUnmountRef = useRef(false);
const { current: scheduledTimers } = useRef(new Set<number>());
useEffect(
() => () => {
didUnmountRef.current = true;
clearAll();
},
[]
);
function clearAll() {
scheduledTimers.forEach((timer) => clearTimeout(timer));
scheduledTimers.clear();
}
return function scheduleTimeout(callback: () => void, delay: number) {
if (didUnmountRef.current) {
throw new Error("Can't schedule a timeout on an unmounted component.");
}
const timer = Number(setTimeout(handleCallback, delay));
scheduledTimers.add(timer);
function handleCallback() {
scheduledTimers.delete(timer);
callback();
}
};
}