From 2924c83717fbf0a7c451867211357e56ab0e9b7e Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Sun, 22 Mar 2026 03:16:02 +0100 Subject: [PATCH] feat: add useDelayedLoading hook to prevent skeleton flicker --- packages/web/src/hooks/useDelayedLoading.ts | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 packages/web/src/hooks/useDelayedLoading.ts diff --git a/packages/web/src/hooks/useDelayedLoading.ts b/packages/web/src/hooks/useDelayedLoading.ts new file mode 100644 index 00000000..11cb9582 --- /dev/null +++ b/packages/web/src/hooks/useDelayedLoading.ts @@ -0,0 +1,49 @@ +import { useState, useEffect, useRef } from 'react'; + +/** + * Gates a loading boolean behind a delay threshold to prevent skeleton flicker. + * - Shows nothing for the first `threshold` ms (default 200) + * - Once shown, keeps skeleton visible for at least `minDisplay` ms (default 300) + */ +export function useDelayedLoading( + isLoading: boolean, + options?: { threshold?: number; minDisplay?: number }, +): boolean { + const threshold = options?.threshold ?? 200; + const minDisplay = options?.minDisplay ?? 300; + + const [show, setShow] = useState(false); + const thresholdRef = useRef>(); + const minDisplayRef = useRef>(); + const displayStartRef = useRef(0); + + useEffect(() => { + if (isLoading) { + thresholdRef.current = setTimeout(() => { + displayStartRef.current = Date.now(); + setShow(true); + }, threshold); + } else { + // Loading finished — clear threshold timer if it hasn't fired yet + clearTimeout(thresholdRef.current); + + if (show) { + // Skeleton is visible — enforce minimum display time + const elapsed = Date.now() - displayStartRef.current; + const remaining = minDisplay - elapsed; + if (remaining > 0) { + minDisplayRef.current = setTimeout(() => setShow(false), remaining); + } else { + setShow(false); + } + } + } + + return () => { + clearTimeout(thresholdRef.current); + clearTimeout(minDisplayRef.current); + }; + }, [isLoading]); // eslint-disable-line react-hooks/exhaustive-deps + + return show; +}