fix: add AbortController and unmount guard to useIpLookup hook
- Add AbortController to cancel pending IP lookups when component unmounts - Prevent state updates on unmounted components by checking abort signal before setState calls - Add useEffect cleanup that aborts pending requests on unmount - Update lookupIp API function to accept optional AbortSignal parameter - Converts callback-based fetch to async/await pattern for better control flow - Aligns with other API functions that already support abort signals (fetchJails, fetchJailBannedIps) Fixes TASK-ABORT-04 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -237,11 +237,12 @@ export async function unbanAllBans(): Promise<UnbanAllResponse> {
|
||||
* Look up ban status and geo-location for an IP address.
|
||||
*
|
||||
* @param ip - IP address to look up.
|
||||
* @param signal - Optional abort signal for request cancellation.
|
||||
* @returns An {@link IpLookupResponse} with ban history and geo info.
|
||||
* @throws {ApiError} On non-2xx responses (400 for invalid IP).
|
||||
*/
|
||||
export async function lookupIp(ip: string): Promise<IpLookupResponse> {
|
||||
return get<IpLookupResponse>(ENDPOINTS.geoLookup(ip));
|
||||
export async function lookupIp(ip: string, signal?: AbortSignal): Promise<IpLookupResponse> {
|
||||
return get<IpLookupResponse>(ENDPOINTS.geoLookup(ip), signal);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* React hook for looking up a single IP address.
|
||||
*/
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { handleFetchError } from "../utils/fetchError";
|
||||
import { lookupIp } from "../api/jails";
|
||||
import type { IpLookupResponse } from "../types/jail";
|
||||
@@ -22,22 +22,34 @@ export function useIpLookup(): UseIpLookupResult {
|
||||
const [result, setResult] = useState<IpLookupResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const lookup = useCallback((ip: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
const lookup = useCallback((ip: string): void => {
|
||||
(async (): Promise<void> => {
|
||||
// Abort any previous lookup
|
||||
abortRef.current?.abort();
|
||||
const ctrl = new AbortController();
|
||||
abortRef.current = ctrl;
|
||||
|
||||
lookupIp(ip)
|
||||
.then((res) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
const res = await lookupIp(ip, ctrl.signal);
|
||||
if (ctrl.signal.aborted) return;
|
||||
setResult(res);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
} catch (err: unknown) {
|
||||
if (ctrl.signal.aborted) return;
|
||||
handleFetchError(err, setError, "Failed to lookup IP");
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
} finally {
|
||||
if (!ctrl.signal.aborted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
})().catch(() => {
|
||||
// Silently ignore abort errors
|
||||
});
|
||||
}, []);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
@@ -45,5 +57,12 @@ export function useIpLookup(): UseIpLookupResult {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
// Cleanup: abort any pending lookup on unmount
|
||||
useEffect(() => {
|
||||
return (): void => {
|
||||
abortRef.current?.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { result, loading, error, lookup, clear };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user