refactor: implement abort controller for fetch requests in multiple components to prevent memory leaks and improve error handling

This commit is contained in:
Julien Froidefond
2026-01-03 21:51:07 +01:00
parent 512e9a480f
commit e903b55a46
7 changed files with 179 additions and 68 deletions

View File

@@ -17,40 +17,53 @@ export function ClientHomePage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = async () => {
setLoading(true);
setError(null);
useEffect(() => {
const abortController = new AbortController();
try {
const response = await fetch("/api/komga/home");
const fetchData = async () => {
setLoading(true);
setError(null);
if (!response.ok) {
const errorData = await response.json();
const errorCode = errorData.error?.code || ERROR_CODES.KOMGA.SERVER_UNREACHABLE;
try {
const response = await fetch("/api/komga/home", {
signal: abortController.signal,
});
// Si la config Komga est manquante, rediriger vers les settings
if (errorCode === ERROR_CODES.KOMGA.MISSING_CONFIG) {
router.push("/settings");
return;
if (!response.ok) {
const errorData = await response.json();
const errorCode = errorData.error?.code || ERROR_CODES.KOMGA.SERVER_UNREACHABLE;
// Si la config Komga est manquante, rediriger vers les settings
if (errorCode === ERROR_CODES.KOMGA.MISSING_CONFIG) {
router.push("/settings");
return;
}
throw new Error(errorCode);
}
throw new Error(errorCode);
const homeData = await response.json();
setData(homeData);
} catch (err) {
// Ignore abort errors (caused by StrictMode cleanup)
if (err instanceof Error && err.name === "AbortError") {
return;
}
logger.error({ err }, "Error fetching home data");
setError(err instanceof Error ? err.message : ERROR_CODES.KOMGA.SERVER_UNREACHABLE);
} finally {
if (!abortController.signal.aborted) {
setLoading(false);
}
}
};
const homeData = await response.json();
setData(homeData);
} catch (err) {
logger.error({ err }, "Error fetching home data");
setError(err instanceof Error ? err.message : ERROR_CODES.KOMGA.SERVER_UNREACHABLE);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return () => {
abortController.abort();
};
}, [router]);
const handleRefresh = async () => {
try {
@@ -79,14 +92,39 @@ export function ClientHomePage() {
enabled: !loading && !error && !!data,
});
const handleRetry = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch("/api/komga/home");
if (!response.ok) {
const errorData = await response.json();
const errorCode = errorData.error?.code || ERROR_CODES.KOMGA.SERVER_UNREACHABLE;
if (errorCode === ERROR_CODES.KOMGA.MISSING_CONFIG) {
router.push("/settings");
return;
}
throw new Error(errorCode);
}
const homeData = await response.json();
setData(homeData);
} catch (err) {
logger.error({ err }, "Error fetching home data");
setError(err instanceof Error ? err.message : ERROR_CODES.KOMGA.SERVER_UNREACHABLE);
} finally {
setLoading(false);
}
};
if (loading) {
return <HomePageSkeleton />;
}
const handleRetry = () => {
fetchData();
};
if (error) {
return (
<main className="container mx-auto px-4 py-8">

View File

@@ -26,6 +26,10 @@ import { NavButton } from "@/components/ui/nav-button";
import { IconButton } from "@/components/ui/icon-button";
import logger from "@/lib/logger";
// Module-level flags to prevent duplicate fetches (survives StrictMode remounts)
let sidebarInitialFetchDone = false;
let sidebarFetchInProgress = false;
interface SidebarProps {
isOpen: boolean;
onClose: () => void;
@@ -112,9 +116,18 @@ export function Sidebar({
}, [toast]);
useEffect(() => {
if (Object.keys(preferences).length > 0) {
refreshLibraries();
refreshFavorites();
// Only load once when preferences become available (module-level flag survives StrictMode)
if (
!sidebarInitialFetchDone &&
!sidebarFetchInProgress &&
Object.keys(preferences).length > 0
) {
sidebarFetchInProgress = true;
sidebarInitialFetchDone = true;
Promise.all([refreshLibraries(), refreshFavorites()]).finally(() => {
sidebarFetchInProgress = false;
});
}
}, [preferences, refreshLibraries, refreshFavorites]);
@@ -138,6 +151,10 @@ export function Sidebar({
const handleLogout = async () => {
try {
// Reset module-level flags to allow refetch on next login
sidebarInitialFetchDone = false;
sidebarFetchInProgress = false;
await signOut({ callbackUrl: "/login" });
setLibraries([]);
setFavorites([]);