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:
@@ -1,40 +1,3 @@
|
||||
## [IMPORTANT] Provider ordering fragility (Frontend)
|
||||
|
||||
**Where found**
|
||||
|
||||
- `frontend/src/App.tsx` — 10-level deep provider nesting
|
||||
- `frontend/src/providers/PROVIDER_ORDER.md` — documents order, no compile-time enforcement
|
||||
|
||||
**Why this is needed**
|
||||
|
||||
Provider order (ThemeProvider → AppContents → FluentProvider → ...) enforced only at runtime. Accidental reorder caught only after deploy.
|
||||
|
||||
**Goal**
|
||||
|
||||
Add compile-time validation of provider ordering.
|
||||
|
||||
**What to do**
|
||||
|
||||
1. Create provider composition utility enforcing order
|
||||
2. Use TypeScript discriminated unions
|
||||
3. Add ESLint rule to check provider wrapping
|
||||
|
||||
**Possible traps and issues**
|
||||
|
||||
- TypeScript doesn't easily enforce ordering
|
||||
- May be overkill — improve runtime error messages instead
|
||||
|
||||
**Docs changes needed**
|
||||
|
||||
- Update `Docs/Architekture.md` § 3.2 (Providers)
|
||||
|
||||
**Doc references**
|
||||
|
||||
- `Docs/Architekture.md` § 3.2 (Providers)
|
||||
- `frontend/src/providers/PROVIDER_ORDER.md`
|
||||
|
||||
---
|
||||
|
||||
## [IMPORTANT] Promise cancellation not checked in .then()/.catch() chains
|
||||
|
||||
**Where found**
|
||||
|
||||
@@ -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