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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user