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.
|
* Look up ban status and geo-location for an IP address.
|
||||||
*
|
*
|
||||||
* @param ip - IP address to look up.
|
* @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.
|
* @returns An {@link IpLookupResponse} with ban history and geo info.
|
||||||
* @throws {ApiError} On non-2xx responses (400 for invalid IP).
|
* @throws {ApiError} On non-2xx responses (400 for invalid IP).
|
||||||
*/
|
*/
|
||||||
export async function lookupIp(ip: string): Promise<IpLookupResponse> {
|
export async function lookupIp(ip: string, signal?: AbortSignal): Promise<IpLookupResponse> {
|
||||||
return get<IpLookupResponse>(ENDPOINTS.geoLookup(ip));
|
return get<IpLookupResponse>(ENDPOINTS.geoLookup(ip), signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* React hook for looking up a single IP address.
|
* 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 { handleFetchError } from "../utils/fetchError";
|
||||||
import { lookupIp } from "../api/jails";
|
import { lookupIp } from "../api/jails";
|
||||||
import type { IpLookupResponse } from "../types/jail";
|
import type { IpLookupResponse } from "../types/jail";
|
||||||
@@ -22,22 +22,34 @@ export function useIpLookup(): UseIpLookupResult {
|
|||||||
const [result, setResult] = useState<IpLookupResponse | null>(null);
|
const [result, setResult] = useState<IpLookupResponse | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
const lookup = useCallback((ip: string) => {
|
const lookup = useCallback((ip: string): void => {
|
||||||
setLoading(true);
|
(async (): Promise<void> => {
|
||||||
setError(null);
|
// Abort any previous lookup
|
||||||
setResult(null);
|
abortRef.current?.abort();
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
abortRef.current = ctrl;
|
||||||
|
|
||||||
lookupIp(ip)
|
setLoading(true);
|
||||||
.then((res) => {
|
setError(null);
|
||||||
|
setResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await lookupIp(ip, ctrl.signal);
|
||||||
|
if (ctrl.signal.aborted) return;
|
||||||
setResult(res);
|
setResult(res);
|
||||||
})
|
} catch (err: unknown) {
|
||||||
.catch((err: unknown) => {
|
if (ctrl.signal.aborted) return;
|
||||||
handleFetchError(err, setError, "Failed to lookup IP");
|
handleFetchError(err, setError, "Failed to lookup IP");
|
||||||
})
|
} finally {
|
||||||
.finally(() => {
|
if (!ctrl.signal.aborted) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
})().catch(() => {
|
||||||
|
// Silently ignore abort errors
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const clear = useCallback(() => {
|
const clear = useCallback(() => {
|
||||||
@@ -45,5 +57,12 @@ export function useIpLookup(): UseIpLookupResult {
|
|||||||
setError(null);
|
setError(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Cleanup: abort any pending lookup on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return (): void => {
|
||||||
|
abortRef.current?.abort();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return { result, loading, error, lookup, clear };
|
return { result, loading, error, lookup, clear };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user