Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 100 additions & 17 deletions src/useFetch.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,122 @@
import { signal, setSignal } from "./signals";
import { signal, setSignal, Signal } from "./signals";

export function useFetch<T = any>(url: string, init?: RequestInit) {
const loading = signal(true);
interface UseFetchResult<T> {
loading: Signal<boolean>;
data: Signal<T | null>;
error: Signal<Error | null>;
refresh: () => Promise<void>;
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<T = any>(
url: string,
options: UseFetchOptions = {}
): UseFetchResult<T> {
const { autoFetch = true, timeout = 10000, ...fetchOptions } = options;

const loading = signal(autoFetch);
const data = signal<T | null>(null);
const error = signal<Error | null>(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<void> => {
// Abort any ongoing request
abort();

async function run() {
setSignal(loading, true);
setSignal(error, null);

controller = new AbortController();

// Set up timeout
timeoutId = window.setTimeout(() => {

Check failure on line 59 in src/useFetch.ts

View workflow job for this annotation

GitHub Actions / build (18)

Unhandled error

ReferenceError: window is not defined ❯ run src/useFetch.ts:59:5 ❯ Module.useFetch src/useFetch.ts:99:5 ❯ tests/useFetch.test.js:10:23 ❯ node_modules/@vitest/runner/dist/index.js:146:14 ❯ node_modules/@vitest/runner/dist/index.js:533:11 ❯ runWithTimeout node_modules/@vitest/runner/dist/index.js:39:7 ❯ runTest node_modules/@vitest/runner/dist/index.js:1056:17 ❯ processTicksAndRejections node:internal/process/task_queues:95:5 ❯ runSuite node_modules/@vitest/runner/dist/index.js:1205:15 ❯ runSuite node_modules/@vitest/runner/dist/index.js:1205:15 This error originated in "tests/useFetch.test.js" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "fetches JSON". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Check failure on line 59 in src/useFetch.ts

View workflow job for this annotation

GitHub Actions / build (18)

Unhandled error

ReferenceError: window is not defined ❯ run src/useFetch.ts:59:5 ❯ Module.useFetch src/useFetch.ts:99:5 ❯ tests/useFetch.test.ts:13:19 ❯ node_modules/@vitest/runner/dist/index.js:146:14 ❯ node_modules/@vitest/runner/dist/index.js:533:11 ❯ runWithTimeout node_modules/@vitest/runner/dist/index.js:39:7 ❯ runTest node_modules/@vitest/runner/dist/index.js:1056:17 ❯ processTicksAndRejections node:internal/process/task_queues:95:5 ❯ runSuite node_modules/@vitest/runner/dist/index.js:1205:15 ❯ runSuite node_modules/@vitest/runner/dist/index.js:1205:15 This error originated in "tests/useFetch.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "fetches JSON". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Check failure on line 59 in src/useFetch.ts

View workflow job for this annotation

GitHub Actions / build (20)

Unhandled error

ReferenceError: window is not defined ❯ run src/useFetch.ts:59:5 ❯ Module.useFetch src/useFetch.ts:99:5 ❯ tests/useFetch.test.js:10:23 ❯ node_modules/@vitest/runner/dist/index.js:146:14 ❯ node_modules/@vitest/runner/dist/index.js:533:11 ❯ runWithTimeout node_modules/@vitest/runner/dist/index.js:39:7 ❯ runTest node_modules/@vitest/runner/dist/index.js:1056:17 ❯ processTicksAndRejections node:internal/process/task_queues:95:5 ❯ runSuite node_modules/@vitest/runner/dist/index.js:1205:15 ❯ runSuite node_modules/@vitest/runner/dist/index.js:1205:15 This error originated in "tests/useFetch.test.js" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "fetches JSON". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Check failure on line 59 in src/useFetch.ts

View workflow job for this annotation

GitHub Actions / build (20)

Unhandled error

ReferenceError: window is not defined ❯ run src/useFetch.ts:59:5 ❯ Module.useFetch src/useFetch.ts:99:5 ❯ tests/useFetch.test.ts:13:19 ❯ node_modules/@vitest/runner/dist/index.js:146:14 ❯ node_modules/@vitest/runner/dist/index.js:533:11 ❯ runWithTimeout node_modules/@vitest/runner/dist/index.js:39:7 ❯ runTest node_modules/@vitest/runner/dist/index.js:1056:17 ❯ processTicksAndRejections node:internal/process/task_queues:95:5 ❯ runSuite node_modules/@vitest/runner/dist/index.js:1205:15 ❯ runSuite node_modules/@vitest/runner/dist/index.js:1205:15 This error originated in "tests/useFetch.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "fetches JSON". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.
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<void> => {
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<T = any>(
url: string,
options: Omit<UseFetchOptions, "autoFetch"> = {}
): UseFetchResult<T> & { execute: () => Promise<void> } {
const result = useFetch<T>(url, { ...options, autoFetch: false });

return {
...result,
execute: result.refresh,
};
}
Loading