feat: Implement global request lifecycle cancellation on route transitions

Adds a navigation-aware request cancellation mechanism that automatically
aborts all route-specific API requests when the user navigates to a
different route. This prevents silent state-update errors from responses
arriving after component unmount and conserves bandwidth by cancelling
now-irrelevant requests.

Key additions:
- NavigationCancellationContext: Context for managing route-specific signals
- NavigationCancellationProvider: Provider that detects route changes and
  aborts all signals from the previous route
- useNavigationAbortSignal hook: Allows components to subscribe to
  navigation-aware cancellation signals
- Comprehensive tests for the cancellation lifecycle
- Documentation in Web-Development.md for request lifecycle policy

The provider is placed in the app hierarchy between BrowserRouter and
AuthProvider, ensuring consistent cancellation behavior across all routes.

Long-lived background tasks (polling, session validation) can opt-out by
managing their own AbortController lifecycle.

Closes #23 from Tasks.md: No global cancellation policy on route transitions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-28 09:58:59 +02:00
parent e0a4d36fc3
commit 7ba1cf7ca2
8 changed files with 476 additions and 53 deletions

View File

@@ -1,23 +1,3 @@
## 22) Magic strings are scattered in frontend storage keys
- Where found:
- [frontend/src/providers/AuthProvider.tsx](frontend/src/providers/AuthProvider.tsx)
- [frontend/src/layouts/MainLayout.tsx](frontend/src/layouts/MainLayout.tsx)
- [frontend/src/providers/ThemeProvider.tsx](frontend/src/providers/ThemeProvider.tsx)
- Why this is needed:
- Repeated literals invite drift and typo regressions.
- Goal:
- Centralize user/session/local storage keys.
- What to do:
- Consolidate into a single constants module.
- Possible traps and issues:
- Existing tests may assume current literal values.
- Docs changes needed:
- Add storage key registry note.
- Doc references:
- [frontend/src/utils/constants.ts](frontend/src/utils/constants.ts)
---
## 23) No global cancellation policy on route transitions
- Where found:
- [frontend/src/hooks](frontend/src/hooks)

View File

@@ -291,6 +291,92 @@ export function useMyCustomData<TResponse, TData>(options: MyOptions): MyResult
- **Testability**: Base hook can be tested in isolation; custom effects are minimal and easy to test
- **Maintainability**: Bug fixes to abort or error handling only need to happen once
### Request Lifecycle & Navigation-Aware Cancellation
BanGUI provides a global cancellation mechanism that automatically aborts route-specific requests when the user navigates away. This prevents:
- Silent errors from responses arriving after component unmount
- Wasted bandwidth from now-irrelevant requests
- State inconsistencies between pages
**How It Works:**
The `NavigationCancellationProvider` (wraps the entire router) detects route changes and aborts all `AbortSignal`s obtained from `useNavigationAbortSignal()`. Each route gets its own set of signals that live for the duration of that route.
**When to Use Navigation Signals:**
Use `useNavigationAbortSignal()` for data fetches that are **specific to the current route**:
- Page-level data fetches (e.g., dashboard stats on the home page)
- User-initiated refetches on the current page
- Paginated lists, search results, filtered data
**When NOT to Use:**
Long-lived background tasks should manage their own lifecycle and NOT use navigation signals:
- Session validation (survives route changes)
- Service-level polling (e.g., server health checks)
- Background syncs that may take longer than a user's stay on a page
- Cross-route background operations
**Usage Pattern:**
```ts
// hooks/useDashboardData.ts
export function useDashboardData(): DashboardResult {
const signal = useNavigationAbortSignal(); // Gets signal for current route
const { items: dashStats } = useListData({
fetcher: (sig) => fetchDashboardStats(sig || signal), // Use both signals
selector: (res) => res.stats,
errorMessage: "Failed to load dashboard stats",
});
return { dashStats };
}
```
When the user navigates away, the signal is automatically aborted, cancelling any in-flight dashboard stats requests.
**Opt-Out for Long-Lived Tasks:**
If a fetch should persist across route changes, do not use `useNavigationAbortSignal()`. Instead, manage your own `AbortController`:
```ts
// Service-level polling — persists across route changes
export function useServerHealth(): HealthResult {
const [health, setHealth] = useState<Health | null>(null);
const controllerRef = useRef<AbortController>(new AbortController());
useEffect(() => {
const poll = async () => {
try {
const data = await fetchHealth(controllerRef.current.signal);
setHealth(data);
} catch (err) {
if (!(err instanceof DOMException && err.name === "AbortError")) {
console.error(err);
}
}
};
const interval = setInterval(poll, 5000);
void poll(); // First fetch immediately
return () => {
clearInterval(interval);
controllerRef.current?.abort();
};
}, []);
return { health };
}
```
**Provider Configuration:**
The `NavigationCancellationProvider` is automatically placed in `App.tsx` inside `BrowserRouter` but before `AuthProvider`. It wraps all routes (including setup and login) to ensure consistent cancellation behavior across the entire app.
See `src/providers/PROVIDER_ORDER.md` for the full provider hierarchy and dependencies.
---
## 4. Code Organization