fix: capture AbortController in local variable to avoid race condition in three hooks
TASK-ABORT-03: Fix stale abortRef read in .finally() callbacks In useGlobalConfig, useServerSettings, and useJailConfigDetail hooks, the .finally() block was reading abortRef.current instead of using the locally captured controller reference. If load() is called while a fetch is in flight, the previous fetch's .finally() would read the new controller (not aborted) and prematurely clear the loading state while the new fetch is still pending. Changes: - useGlobalConfig.ts: use locally-captured ctrl in .finally() (line 46) - useServerSettings.ts: use locally-captured ctrl in .finally() (line 50) - useJailConfigDetail.ts: use locally-captured ctrl in .finally() (line 47) All three hooks already use ctrl correctly in .then() and .catch() callbacks. Documentation: - Add 'AbortController in Hooks' section to Web-Development.md - Explains the pattern and shows incorrect vs correct examples - Prevents future regressions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -373,6 +373,42 @@ function useBans(hours: number): UseBansResult {
|
||||
export default useBans;
|
||||
```
|
||||
|
||||
### AbortController in Hooks
|
||||
|
||||
When using `AbortController` for fetch cancellation in hooks with mutable refs:
|
||||
|
||||
- **Always** capture the controller in a local `const` variable before the async operation.
|
||||
- Use that **local variable** in all callbacks (`.then()`, `.catch()`, `.finally()`), never read `abortRef.current` from inside an async callback.
|
||||
- This prevents race conditions: if `load()` is called while a fetch is in flight, the previous fetch's callbacks will use the old, locally-captured controller reference, not the newly-assigned one.
|
||||
|
||||
Incorrect (reads `abortRef.current` in callback — this is racy):
|
||||
```ts
|
||||
const load = useCallback(() => {
|
||||
const ctrl = new AbortController();
|
||||
abortRef.current = ctrl;
|
||||
fetchData()
|
||||
.finally(() => {
|
||||
if (!abortRef.current?.signal.aborted) { // ❌ Wrong: reads mutable ref
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
```
|
||||
|
||||
Correct (uses local `ctrl` in all callbacks):
|
||||
```ts
|
||||
const load = useCallback(() => {
|
||||
const ctrl = new AbortController();
|
||||
abortRef.current = ctrl;
|
||||
fetchData()
|
||||
.finally(() => {
|
||||
if (!ctrl.signal.aborted) { // ✅ Correct: uses locally-captured variable
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Naming Conventions
|
||||
|
||||
Reference in New Issue
Block a user