sendou.ink/app/hooks/common.ts
2022-02-18 08:26:29 +02:00

127 lines
3.3 KiB
TypeScript

import * as React from "react";
import { useLoaderData, useMatches, useNavigate } from "remix";
import type { SSETarget } from "server/events";
import { z } from "zod";
import { LoggedInUserSchema } from "~/utils/schemas";
export const useUser = () => {
const [root] = useMatches();
const parsed = LoggedInUserSchema.parse(root.data);
return parsed?.user;
};
export const useBaseURL = () => {
const [root] = useMatches();
const parsed = z.object({ baseURL: z.string() }).parse(root.data);
return parsed.baseURL;
};
export const useEvents = (
target: SSETarget,
handleEvent: (data: unknown) => void
) => {
React.useEffect(() => {
const source = new EventSource(
`/events?${new URLSearchParams(target).toString()}`
);
source.addEventListener("open", () => {
console.log("SSE opened!");
});
source.addEventListener("message", (e) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
handleEvent(JSON.parse(e.data));
});
source.addEventListener("error", (e) => {
console.error("SSE error: ", e);
});
return () => {
source.close();
};
}, []);
};
// TODO: fix causes memory leak
/** @link https://stackoverflow.com/a/64983274 */
export const useTimeoutState = <T>(
defaultState: T
): [
T,
(action: React.SetStateAction<T>, opts?: { timeout: number }) => void
] => {
const [state, _setState] = React.useState<T>(defaultState);
const [currentTimeoutId, setCurrentTimeoutId] = React.useState<
NodeJS.Timeout | undefined
>();
const setState = React.useCallback(
(action: React.SetStateAction<T>, opts?: { timeout: number }) => {
if (currentTimeoutId != null) {
clearTimeout(currentTimeoutId);
}
_setState(action);
const id = setTimeout(
() => _setState(defaultState),
opts?.timeout ?? 4000
);
setCurrentTimeoutId(id);
},
[currentTimeoutId, defaultState]
);
return [state, setState];
};
/** @link https://usehooks.com/useOnClickOutside/ */
export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
ref: React.RefObject<T>,
handler: (event: MouseEvent | TouchEvent) => void
) {
React.useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]);
}
/** Refreshed loader data of the current route in an interval.
* @returns Timestamp last updated
*/
export function usePolling(pollingActive = true) {
const [lastUpdated, setLastUpdated] = React.useState(new Date());
const data = useLoaderData<unknown>();
const navigate = useNavigate();
const INTERVAL = 30_000; // 30 seconds
React.useEffect(() => {
if (!pollingActive) return;
const timer = setTimeout(() => {
navigate(".");
}, INTERVAL);
return () => clearTimeout(timer);
}, [pollingActive, navigate, data]);
React.useEffect(() => {
setLastUpdated(new Date());
}, [data]);
return lastUpdated;
}