Add AbortController cleanup to async frontend effects

This commit is contained in:
2026-04-18 21:30:57 +02:00
parent 2105f8b435
commit 6c053cdaee
16 changed files with 128 additions and 37 deletions

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { Button, Field, Input, MessageBar, MessageBarBody, Select, Spinner, Text } from "@fluentui/react-components";
import { PlayRegular } from "@fluentui/react-icons";
import { useCommonSectionStyles } from "../../theme/commonStyles";
@@ -25,6 +25,7 @@ export function BlocklistScheduleSection({ onRunImport, runImportRunning }: Sche
const { info, loading, error, saveSchedule } = useSchedule();
const [saving, setSaving] = useState(false);
const [saveMsg, setSaveMsg] = useState<string | null>(null);
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const config = info?.config ?? {
frequency: "daily" as ScheduleFrequency,
@@ -37,12 +38,20 @@ export function BlocklistScheduleSection({ onRunImport, runImportRunning }: Sche
const [draft, setDraft] = useState<ScheduleConfig>(config);
const handleSave = useCallback((): void => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = null;
}
setSaving(true);
saveSchedule(draft)
.then(() => {
setSaveMsg("Schedule saved.");
setSaving(false);
setTimeout(() => { setSaveMsg(null); }, 3000);
saveTimeoutRef.current = setTimeout(() => {
setSaveMsg(null);
saveTimeoutRef.current = null;
}, 3000);
})
.catch((err: unknown) => {
setSaveMsg(err instanceof Error ? err.message : "Failed to save schedule");
@@ -50,6 +59,14 @@ export function BlocklistScheduleSection({ onRunImport, runImportRunning }: Sche
});
}, [draft, saveSchedule]);
useEffect(() => {
return (): void => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
};
}, []);
return (
<div className={sectionStyles.section}>
<div className={sectionStyles.sectionHeader}>

View File

@@ -106,12 +106,14 @@ export function ActivateJailDialog({
useEffect(() => {
if (!open || !jail) return;
const controller = new AbortController();
setValidating(true);
setValidationIssues([]);
setValidationWarnings([]);
onValidate()
.then((result) => {
if (controller.signal.aborted) return;
setValidationIssues(result.issues);
})
.catch(() => {
@@ -119,8 +121,13 @@ export function ActivateJailDialog({
// attempt activation and let the server decide.
})
.finally(() => {
if (controller.signal.aborted) return;
setValidating(false);
});
return (): void => {
controller.abort();
};
}, [open, jail, onValidate]);
const handleClose = (): void => {

View File

@@ -75,25 +75,33 @@ export function ConfFilesTab({
const [newContent, setNewContent] = useState("");
const [creating, setCreating] = useState(false);
const loadFiles = useCallback(async () => {
const loadFiles = useCallback(async (signal?: AbortSignal) => {
setLoading(true);
setError(null);
try {
const resp = await fetchList();
if (signal?.aborted) return;
setFiles(resp.files);
} catch (err: unknown) {
if (signal?.aborted) return;
setError(
err instanceof ApiError
? err.message
: `Failed to load ${label.toLowerCase()} files.`,
);
} finally {
setLoading(false);
if (!signal?.aborted) {
setLoading(false);
}
}
}, [fetchList, label]);
useEffect(() => {
void loadFiles();
const controller = new AbortController();
void loadFiles(controller.signal);
return (): void => {
controller.abort();
};
}, [loadFiles]);
const handleAccordionToggle = useCallback(