Skip to content

Commit

Permalink
Merge pull request altalyst-solutions#7 from sleepinzombie/feat/add-u…
Browse files Browse the repository at this point in the history
…se-api

Add useApi hook
  • Loading branch information
sleepinzombie authored Nov 3, 2024
2 parents 01e3c45 + ecf24c8 commit 4d546dc
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 12 deletions.
105 changes: 97 additions & 8 deletions src/hooks/use-api.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,102 @@
export const useApi = () => {
console.log("Inside useApi hook.");
import { useEffect, useState } from "react";

return "useApi called";
};
/**
* Options for configuring the API request.
* @interface UseApiOptions
*/
interface UseApiOptions {
/** The HTTP method to use for the request. Default is `GET`. */
method?: "GET" | "POST" | "PUT" | "DELETE";
/** Headers to include in the request. */
headers?: HeadersInit;
/** The body of the request for methods that require it (e.g., `POST`, `PUT`). */
body?: BodyInit;
}

export function helloAnything(thing: string): string {
return `Hello ${thing}!`;
/**
* The return type of the useApi hook.
* @template T
* @interface UseApiReturn
*/
interface UseApiReturn<T> {
/** The fetched data, or null if there’s no data yet. */
data: T | null;
/** Indicates whether the request is currently being processed. */
loading: boolean;
/** Contains any error message if the request fails, or null if no error occurred. */
error: string | null;
/** Function to manually refetch the data. */
refetch: () => Promise<void>;
}

export type TTest = {
text: string;
/**
* Custom React hook to perform API requests.
*
* This hook simplifies the process of making HTTP requests from a React component,
* managing the loading state, data, and error handling for the request lifecycle.
*
* On initialization, the hook triggers a fetch request to the specified URL
* and tracks the loading status until the request is complete. It also provides
* a `refetch` function to allow manual re-execution of the request, useful
* in scenarios where data may need to be refreshed.
*
* The hook can handle different HTTP methods and allows for custom headers
* and request bodies, making it flexible for various API interactions.
*
* The response data is automatically parsed as JSON, and any errors encountered
* during the request process are caught and returned, allowing the consuming
* component to handle them appropriately.
*
* @template T The type of data expected in the response. This helps ensure type safety
* when consuming the data in a TypeScript environment.
* @param {string} url - The endpoint from which to fetch data. This should be a fully
* qualified URL that the application can reach.
* @param {UseApiOptions} [options] - Options for configuring the request, including
* HTTP method, headers, and body content.
* @returns {UseApiReturn<T>} The result of the API request, including:
* - `data`: The fetched data, or null if there’s no data yet.
* - `loading`: Indicates whether the request is currently being processed.
* - `error`: Contains any error message if the request fails, or null if no error occurred.
* - `refetch`: Function to manually refetch the data, useful for refreshing data in UI.
*
* @example
* const { data, loading, error, refetch } = useApi<MyDataType>('https://api.example.com/data');
*/
export const useApi = <T = unknown>(
url: string,
options?: UseApiOptions
): UseApiReturn<T> => {
const [loading, setLoading] = useState<boolean>(true);
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<string | null>(null);

const fetchApi = async () => {
setLoading(true);
setError(null);

try {
const response = await fetch(url, {
method: options?.method || "GET",
headers: options?.headers,
body: options?.body,
});

if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}

const result: T = await response.json();
setData(result);
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};

useEffect(() => {
fetchApi();
}, [url, options?.method, options?.headers, options?.body]);

return { data, loading, error, refetch: fetchApi };
};
96 changes: 93 additions & 3 deletions tests/hooks/use-api.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,96 @@
import { useApi } from "@/hooks";
import { expect, test } from "vitest";
import { act, renderHook } from "@testing-library/react";
import type { Mock } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";

test("return default value", () => {
expect(useApi()).toBe("useApi called");
interface MockResponse {
success: boolean;
}

global.fetch = vi.fn();

describe("useApi", () => {
const mockUrl = "https://api.example.com/data";
const mockData: MockResponse = { success: true };

beforeEach(() => {
vi.clearAllMocks();
});

it("should initialize with loading state and no data or error", async () => {
(fetch as Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockData,
});

const { result } = renderHook(() => useApi<MockResponse>(mockUrl));

expect(result.current.loading).toBe(true);
expect(result.current.data).toBeNull();
expect(result.current.error).toBeNull();

await act(async () => {});

expect(result.current.loading).toBe(false);
expect(result.current.data).toEqual(mockData);
expect(result.current.error).toBeNull();
});

it("should fetch data successfully and update data state", async () => {
(fetch as Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockData,
});

const { result } = renderHook(() => useApi<MockResponse>(mockUrl));

await act(async () => {});

expect(result.current.loading).toBe(false);
expect(result.current.data).toEqual(mockData);
expect(result.current.error).toBeNull();
});

it("should set error state when the fetch fails", async () => {
(fetch as Mock).mockResolvedValueOnce({
ok: false,
status: 404,
});

const { result } = renderHook(() => useApi<MockResponse>(mockUrl));

await act(async () => {});

expect(result.current.loading).toBe(false);
expect(result.current.data).toBeNull();
expect(result.current.error).toBe("Error: 404");
});

it("should allow refetching data", async () => {
(fetch as Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockData,
});

const { result } = renderHook(() => useApi<MockResponse>(mockUrl));

await act(async () => {});

expect(result.current.data).toEqual(mockData);

// Simulate data update on refetch
const newMockData: MockResponse = { success: false };
(fetch as Mock).mockResolvedValueOnce({
ok: true,
json: async () => newMockData,
});

await act(async () => {
await result.current.refetch();
});

expect(result.current.data).toEqual(newMockData);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBeNull();
});
});
5 changes: 4 additions & 1 deletion tests/setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";

// Runs a clean after each test case
afterEach(() => {});
afterEach(() => {
cleanup();
});

0 comments on commit 4d546dc

Please sign in to comment.