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

@@ -1046,6 +1046,64 @@ function useBans(hours: number): UseBansResult {
export default useBans;
```
### Promise Cancellation in Callbacks
When performing async operations in `useCallback` (form submissions, button clicks, etc.), always check if the operation was cancelled before updating state. This prevents React warnings and memory leaks when components unmount or users navigate away.
**Problem with naked `.then()` chains:**
```tsx
const handleSubmit = useCallback((values: FormValues) => {
setSaving(true);
saveData(values)
.then(() => {
setSaving(false); // ❌ Updates state even if component unmounted
setDialogOpen(false);
})
.catch((err) => {
setSaving(false); // ❌ Updates state on unmounted component
setError(err.message);
});
}, []);
```
**Solution: Use AbortController to track and cancel operations:**
```tsx
const submitControllerRef = useRef<AbortController | null>(null);
const handleSubmit = useCallback((values: FormValues) => {
// Abort any previous in-flight request
submitControllerRef.current?.abort();
const controller = new AbortController();
submitControllerRef.current = controller;
setSaving(true);
saveData(values)
.then(() => {
if (controller.signal.aborted) return; // ✅ Check before state updates
setSaving(false);
setDialogOpen(false);
})
.catch((err) => {
if (controller.signal.aborted) return; // ✅ Check before state updates
setSaving(false);
setError(err.message);
});
}, []);
// Clean up on unmount
useEffect(() => {
return () => {
submitControllerRef.current?.abort(); // ✅ Abort in cleanup
};
}, []);
```
**Key rules:**
- Always create a **local `const`** variable to capture the controller — never read `ref.current` inside `.then()` callbacks (race condition risk).
- Check `controller.signal.aborted` **in every `.then()` and `.catch()` block** before calling `setState`.
- Abort any previous operation before starting a new one to avoid stale callbacks.
- Clean up with a `useEffect` return function on component unmount.
### AbortController in Hooks
When using `AbortController` for fetch cancellation in hooks with mutable refs: