feat: add useDelayedLoading hook to prevent skeleton flicker
This commit is contained in:
@@ -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<ReturnType<typeof setTimeout>>();
|
||||
const minDisplayRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user