Fix promise cancellation in 5 components with AbortController refs

Add AbortController refs and abort signal checks to prevent race conditions
and memory leaks when components unmount or new requests are initiated.

Components fixed:
- JailsTab.tsx: validation handler with AbortController pattern
- JailInfoSection.tsx: handle function with useCallback wrapper
- RawConfigSection.tsx: fetch handler with abort checks
- ConfFilesTab.tsx: file fetch handler with abort signal verification
- IgnoreListSection.tsx: three handlers (add, remove, toggle) with callbacks

All handlers now:
1. Abort previous requests before initiating new ones
2. Create and store new AbortController instances
3. Check abort status before state updates in .then()/.catch()
4. Include cleanup effects that abort on unmount

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-01 17:43:47 +02:00
parent c988b4b8b6
commit 96a21ffb70
15 changed files with 291 additions and 73 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import {
Badge,
Button,
@@ -35,25 +35,54 @@ export function IgnoreListSection({
const sectionStyles = useCommonSectionStyles();
const [inputVal, setInputVal] = useState("");
const [opError, setOpError] = useState<string | null>(null);
const opControllerRef = useRef<AbortController | null>(null);
const handleAdd = (): void => {
const handleAdd = useCallback((): void => {
if (!inputVal.trim()) return;
opControllerRef.current?.abort();
const controller = new AbortController();
opControllerRef.current = controller;
setOpError(null);
onAdd(inputVal.trim())
.then(() => {
if (controller.signal.aborted) return;
setInputVal("");
})
.catch((err: unknown) => {
if (controller.signal.aborted) return;
setOpError(err instanceof Error ? err.message : String(err));
});
};
}, [inputVal, onAdd]);
const handleRemove = useCallback((ip: string): void => {
opControllerRef.current?.abort();
const controller = new AbortController();
opControllerRef.current = controller;
const handleRemove = (ip: string): void => {
setOpError(null);
onRemove(ip).catch((err: unknown) => {
if (controller.signal.aborted) return;
setOpError(err instanceof Error ? err.message : String(err));
});
};
}, [onRemove]);
const handleToggleIgnoreSelf = useCallback((checked: boolean): void => {
opControllerRef.current?.abort();
const controller = new AbortController();
opControllerRef.current = controller;
onToggleIgnoreSelf(checked).catch((err: unknown) => {
if (controller.signal.aborted) return;
setOpError(err instanceof Error ? err.message : String(err));
});
}, [onToggleIgnoreSelf]);
useEffect(() => {
return (): void => {
opControllerRef.current?.abort();
};
}, []);
return (
<div className={sectionStyles.section}>
@@ -70,9 +99,7 @@ export function IgnoreListSection({
label="Ignore self — exclude this server's own IP addresses from banning"
checked={ignoreSelf}
onChange={(_e, data): void => {
onToggleIgnoreSelf(data.checked).catch((err: unknown) => {
setOpError(err instanceof Error ? err.message : String(err));
});
handleToggleIgnoreSelf(data.checked);
}}
/>

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
Badge,
@@ -32,24 +32,40 @@ export function JailInfoSection({ jail, onRefresh, onStart, onStop, onSetIdle, o
const sectionStyles = useCommonSectionStyles();
const navigate = useNavigate();
const [ctrlError, setCtrlError] = useState<string | null>(null);
const controllerRef = useRef<AbortController | null>(null);
const handle =
(fn: () => Promise<unknown>, postNavigate = false) =>
(): void => {
setCtrlError(null);
fn()
.then(() => {
if (postNavigate) {
navigate("/jails");
} else {
onRefresh();
}
})
.catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
setCtrlError(msg);
});
const handle = useCallback(
(fn: () => Promise<unknown>, postNavigate = false): (() => void) => {
return (): void => {
controllerRef.current?.abort();
const controller = new AbortController();
controllerRef.current = controller;
setCtrlError(null);
fn()
.then(() => {
if (controller.signal.aborted) return;
if (postNavigate) {
navigate("/jails");
} else {
onRefresh();
}
})
.catch((err: unknown) => {
if (controller.signal.aborted) return;
const msg = err instanceof Error ? err.message : String(err);
setCtrlError(msg);
});
};
},
[navigate, onRefresh]
);
useEffect(() => {
return (): void => {
controllerRef.current?.abort();
};
}, []);
return (
<div className={sectionStyles.section}>