From fe907fb5a6a427fd2a0aeae18361583d63ae5c72 Mon Sep 17 00:00:00 2001 From: luxurybird Date: Thu, 27 Nov 2025 05:12:30 +1000 Subject: [PATCH] refactor with type strict mode --- src/useFetch.ts | 117 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 100 insertions(+), 17 deletions(-) diff --git a/src/useFetch.ts b/src/useFetch.ts index ecb73dc..d565757 100644 --- a/src/useFetch.ts +++ b/src/useFetch.ts @@ -1,39 +1,122 @@ -import { signal, setSignal } from "./signals"; +import { signal, setSignal, Signal } from "./signals"; -export function useFetch(url: string, init?: RequestInit) { - const loading = signal(true); +interface UseFetchResult { + loading: Signal; + data: Signal; + error: Signal; + refresh: () => Promise; + abort: () => void; +} + +interface UseFetchOptions extends RequestInit { + /** + * Whether to automatically execute the fetch on hook creation + * @default true + */ + autoFetch?: boolean; + + /** + * Timeout in milliseconds after which the request will be aborted + * @default 10000 (10 seconds) + */ + timeout?: number; +} + +export function useFetch( + url: string, + options: UseFetchOptions = {} +): UseFetchResult { + const { autoFetch = true, timeout = 10000, ...fetchOptions } = options; + + const loading = signal(autoFetch); const data = signal(null); const error = signal(null); - const controller = new AbortController(); + let controller: AbortController | null = null; + let timeoutId: number | null = null; + + const abort = () => { + if (controller) { + controller.abort(); + controller = null; + } + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + }; + + const run = async (): Promise => { + // Abort any ongoing request + abort(); - async function run() { setSignal(loading, true); + setSignal(error, null); + + controller = new AbortController(); + + // Set up timeout + timeoutId = window.setTimeout(() => { + if (controller) { + controller.abort(); + setSignal(error, new Error("Request timeout")); + setSignal(loading, false); + } + }, timeout); + try { - const res = await fetch(url, { - ...init, + const response = await fetch(url, { + ...fetchOptions, signal: controller.signal, }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } - const json = (await res.json()) as T; - setSignal(data, json); - setSignal(error, null); - } catch (err: any) { - setSignal(error, err); + const responseData = (await response.json()) as T; + setSignal(data, responseData); + } catch (err: unknown) { + // Only update error state if not aborted by user + if ( + !controller?.signal.aborted || + (err instanceof Error && err.name !== "AbortError") + ) { + setSignal(error, err instanceof Error ? err : new Error(String(err))); + } } finally { setSignal(loading, false); + abort(); // Clean up } - } + }; + + const refresh = async (): Promise => { + return run(); + }; - run(); + // Auto-fetch on mount if enabled + if (autoFetch) { + run(); + } return { loading, data, error, - refresh: run, - abort: () => controller.abort(), + refresh, + abort, + }; +} + +// Optional: Hook with manual execution +export function useLazyFetch( + url: string, + options: Omit = {} +): UseFetchResult & { execute: () => Promise } { + const result = useFetch(url, { ...options, autoFetch: false }); + + return { + ...result, + execute: result.refresh, }; }