SurfSense/surfsense_web/hooks/use-global-loading.ts
Anish Sarkar 66a3c877ef feat: replace unified loading approach with global loading provider and refactor loading handling across components
- Introduced GlobalLoadingProvider to manage a consistent loading overlay across the application.
- Replaced existing loading implementations with useGlobalLoadingEffect hook for better control and to prevent animation resets.
- Updated components such as LoginPage, AuthCallbackPage, and DashboardLayout to utilize the new global loading mechanism.
- Removed UnifiedLoadingScreen component to streamline loading management and enhance user experience.
2026-01-25 16:15:25 +05:30

104 lines
3.2 KiB
TypeScript

"use client";
import { useAtom } from "jotai";
import { useCallback, useEffect, useRef } from "react";
import { globalLoadingAtom } from "@/atoms/ui/loading.atoms";
// Global counter to generate unique IDs for each loading request
let loadingIdCounter = 0;
// Track the current active loading ID globally
let currentLoadingId: number | null = null;
// Pending hide timeout - allows new loading requests to take over before hiding
let pendingHideTimeout: ReturnType<typeof setTimeout> | null = null;
/**
* Hook to control the global loading screen.
* The spinner is always mounted in the DOM to prevent animation reset.
*/
export function useGlobalLoading() {
const [loading, setLoading] = useAtom(globalLoadingAtom);
const show = useCallback(
(message?: string, variant: "login" | "default" = "default") => {
// Cancel any pending hide - new loading request takes over
if (pendingHideTimeout) {
clearTimeout(pendingHideTimeout);
pendingHideTimeout = null;
}
const id = ++loadingIdCounter;
currentLoadingId = id;
setLoading({ isLoading: true, message, variant });
return id;
},
[setLoading]
);
const hide = useCallback(
(id?: number) => {
// Only hide if this is the current loading, or if no ID provided (force hide)
if (id === undefined || id === currentLoadingId) {
// Use a small delay to allow React to flush pending mounts
// This prevents flash when transitioning between loading states
if (pendingHideTimeout) {
clearTimeout(pendingHideTimeout);
}
pendingHideTimeout = setTimeout(() => {
// Double-check we're still the current loading after the delay
if (id === undefined || id === currentLoadingId) {
currentLoadingId = null;
setLoading({ isLoading: false, message: undefined, variant: "default" });
}
pendingHideTimeout = null;
}, 50); // Small delay to allow next component to mount and show loading
}
},
[setLoading]
);
return { show, hide, isLoading: loading.isLoading };
}
/**
* Hook that automatically shows/hides the global loading screen based on a condition.
* Useful for components that show loading on mount and hide on unmount.
*
* Uses ownership tracking to prevent flashes when multiple components
* transition loading states (e.g., layout → page).
*
* @param shouldShow - Whether the loading screen should be visible
* @param message - Optional message to display
* @param variant - Visual style variant ("login" or "default")
*/
export function useGlobalLoadingEffect(
shouldShow: boolean,
message?: string,
variant: "login" | "default" = "default"
) {
const { show, hide } = useGlobalLoading();
const loadingIdRef = useRef<number | null>(null);
useEffect(() => {
if (shouldShow) {
// Show loading and store the ID
loadingIdRef.current = show(message, variant);
} else if (loadingIdRef.current !== null) {
// Only hide if we were the ones showing loading
hide(loadingIdRef.current);
loadingIdRef.current = null;
}
}, [shouldShow, message, variant, show, hide]);
// Cleanup on unmount - only hide if we're still the active loading
useEffect(() => {
return () => {
if (loadingIdRef.current !== null) {
hide(loadingIdRef.current);
loadingIdRef.current = null;
}
};
}, [hide]);
}