From 2c0d42d0bc8c1d455c8dde4794acf2441754c654 Mon Sep 17 00:00:00 2001 From: Sainul Abid Date: Fri, 31 May 2024 15:41:45 +0530 Subject: [PATCH 1/3] patch: hooks param type changed AxiosInstance to CreateDefaultConfig --- src/index.ts | 6 +++--- src/test/test.tsx | 10 +++++----- src/types.ts | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index e13ada7..575c346 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,7 @@ import { createConfig, defaultConfig } from './utils' import { ApiClientContext, ApiClientProvider } from './hook' const createClient = (config?: CreateAxiosDefaults) => { - const api: AxiosInstance = axios.create(config) + const api: AxiosInstance = axios.create(config || {}) api.interceptors.response.use( (response) => { return response @@ -41,14 +41,14 @@ export const useApiClient = () => { } // The core hook -export const useApi: UseApi = (instance): UseApiResponse => { +export const useApi: UseApi = (instanceConfig): UseApiResponse => { const [fetching, setFetching] = useState(false) const [data, setData] = useState(null) const [error, setError] = useState(null) const [success, setSuccess] = useState(false) const hook = useApiClient() - const apiClient = instance || hook + const apiClient = instanceConfig ? createClient(instanceConfig) : hook let requestConfig: RequestConfig = {} const fetchData: FetchData = (endpoint, config) => { diff --git a/src/test/test.tsx b/src/test/test.tsx index 865fc10..9cce80e 100644 --- a/src/test/test.tsx +++ b/src/test/test.tsx @@ -36,7 +36,7 @@ describe('UseApi with custom instance', () => { test('should handle successful API call, fetchData', async () => { mock.onGet('/get').reply(200, mockResponse) - const { result } = renderHook(() => useApi(api)) + const { result } = renderHook(() => useApi(api.defaults)) act(() => result.current.fetchData('/get')) expect(result.current.fetching).toBe(true) @@ -52,7 +52,7 @@ describe('UseApi with custom instance', () => { mock.onPost('/post', body).reply(200, postResponse) - const { result } = renderHook(() => useApi(api)) + const { result } = renderHook(() => useApi(api.defaults)) act(() => result.current.mutate('/post', body)) expect(result.current.fetching).toBe(true) @@ -65,7 +65,7 @@ describe('UseApi with custom instance', () => { test('should handle successful API call, Mutate without body, Delete Method', async () => { mock.onDelete('/post?id=1', {}).reply(200, deleteResponse) - const { result } = renderHook(() => useApi(api)) + const { result } = renderHook(() => useApi(api.defaults)) act(() => result.current.mutate('/post?id=1', {}, { method: 'DELETE' })) expect(result.current.fetching).toBe(true) @@ -78,7 +78,7 @@ describe('UseApi with custom instance', () => { test('should handle errors', async () => { mock.onAny('/long').networkError() - const { result } = renderHook(() => useApi(api)) + const { result } = renderHook(() => useApi(api.defaults)) act(() => result.current.mutate('/long')) expect(result.current.fetching).toBe(true) @@ -115,7 +115,7 @@ describe('UseApi with custom instance', () => { describe('UseFormApi hook', () => { test('should handle successful Api call with submit form ', async () => { mock.onPost('/submit', formdata).reply(200, postResponse) - const { result } = renderHook(() => useFormApi(api)) + const { result } = renderHook(() => useFormApi(api.defaults)) act(() => { result.current.submitForm('/submit', formdata) diff --git a/src/types.ts b/src/types.ts index e4adef8..9f91869 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { AxiosInstance, AxiosRequestConfig, Method } from 'axios' +import { AxiosRequestConfig, CreateAxiosDefaults, Method } from 'axios' -export type UseForm = (instance?: AxiosInstance) => UseFormResponse +export type UseForm = (instance?: CreateAxiosDefaults) => UseFormResponse export type UseFormResponse = { submitForm: SubmitForm @@ -37,4 +37,4 @@ export type UseApiSubmit = { } export type AsyncApi = (endpoint: string, method?: Method, config?: RequestConfig) => Promise -export type UseApi = (instance?: AxiosInstance) => UseApiResponse +export type UseApi = (instance?: CreateAxiosDefaults) => UseApiResponse From 6b4193e9ca33ecb8f9c02b0b3170e4d177495904 Mon Sep 17 00:00:00 2001 From: Sainul Abid Date: Sat, 1 Jun 2024 12:32:18 +0530 Subject: [PATCH 2/3] patch: test added, optimized --- package.json | 2 +- src/{hook.tsx => Provider.tsx} | 0 src/hook.ts | 11 ++++ src/index.ts | 16 ++--- src/test/test.tsx | 113 +++++++++++++++++++++++---------- 5 files changed, 96 insertions(+), 46 deletions(-) rename src/{hook.tsx => Provider.tsx} (100%) create mode 100644 src/hook.ts diff --git a/package.json b/package.json index 342441b..1821a9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "useipa", - "version": "0.3.0", + "version": "0.3.1", "description": "A react hook for api fetching", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/src/hook.tsx b/src/Provider.tsx similarity index 100% rename from src/hook.tsx rename to src/Provider.tsx diff --git a/src/hook.ts b/src/hook.ts new file mode 100644 index 0000000..8294737 --- /dev/null +++ b/src/hook.ts @@ -0,0 +1,11 @@ +import { useContext } from 'react' +import { createClient } from '.' +import { ApiClientContext } from './Provider' + +export const useApiClient = () => { + const context = useContext(ApiClientContext) + if (!context) { + return createClient() + } + return createClient(context) +} diff --git a/src/index.ts b/src/index.ts index 575c346..6f5337c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { useContext, useState } from 'react' +import { useState } from 'react' import { AsyncApi, FetchData, @@ -12,12 +12,15 @@ import { } from './types' import axios, { AxiosInstance, AxiosResponse, CreateAxiosDefaults } from 'axios' import { createConfig, defaultConfig } from './utils' -import { ApiClientContext, ApiClientProvider } from './hook' +import { ApiClientProvider } from './Provider' +import { useApiClient } from './hook' -const createClient = (config?: CreateAxiosDefaults) => { +export const createClient = (config?: CreateAxiosDefaults) => { const api: AxiosInstance = axios.create(config || {}) api.interceptors.response.use( (response) => { + console.log(response.config) + return response }, (error) => { @@ -32,13 +35,6 @@ const createClient = (config?: CreateAxiosDefaults) => { export const api = createClient() // This for only for hooks not asyncApi -export const useApiClient = () => { - const context = useContext(ApiClientContext || null) - if (!context) { - return createClient() - } - return createClient(context) -} // The core hook export const useApi: UseApi = (instanceConfig): UseApiResponse => { diff --git a/src/test/test.tsx b/src/test/test.tsx index 9cce80e..d3fb9d2 100644 --- a/src/test/test.tsx +++ b/src/test/test.tsx @@ -1,10 +1,21 @@ import { renderHook, act, waitFor } from '@testing-library/react' -import { api, asyncApi, useApi, useFormApi } from '../' +import { ApiClientProvider, api, asyncApi, useApi, useFormApi } from '../' import MockAdapter from 'axios-mock-adapter' import '@testing-library/jest-dom' +import { useApiClient } from '../hook' +import axios from 'axios' +import { UseApiResponse, UseFormResponse } from '../types' + +// ckeckSuccess function params type +type ResponseType = typeof postResponse | typeof deleteResponse | typeof fetchResponse +type Result = { + current: UseApiResponse | UseFormResponse +} + +jest.mock('../hook') let mock: MockAdapter -const mockResponse = { +const fetchResponse = { userId: 1, id: 1, title: 'delectus aut autem', @@ -24,6 +35,7 @@ describe('UseApi with custom instance', () => { afterEach(() => { mock.reset() }) + test('should return the initial values for data, error and loading', async () => { const { result } = renderHook(() => useApi()) const { data, fetching, error, success } = result.current @@ -34,16 +46,14 @@ describe('UseApi with custom instance', () => { }) test('should handle successful API call, fetchData', async () => { - mock.onGet('/get').reply(200, mockResponse) + mock.onGet('/get').reply(200, fetchResponse) const { result } = renderHook(() => useApi(api.defaults)) act(() => result.current.fetchData('/get')) expect(result.current.fetching).toBe(true) await waitFor(() => { - expect(result.current.success).toBe(true) - expect(result.current.data).toEqual(mockResponse) - expect(result.current.error).toBe(null) + checkSuccess(result, fetchResponse) }) }) @@ -57,9 +67,7 @@ describe('UseApi with custom instance', () => { expect(result.current.fetching).toBe(true) await waitFor(() => { - expect(result.current.success).toBe(true) - expect(result.current.data).toEqual(postResponse) - expect(result.current.error).toBe(null) + checkSuccess(result) }) }) @@ -70,9 +78,7 @@ describe('UseApi with custom instance', () => { expect(result.current.fetching).toBe(true) await waitFor(() => { - expect(result.current.success).toBe(true) - expect(result.current.data).toEqual(deleteResponse) - expect(result.current.error).toBe(null) + checkSuccess(result, deleteResponse) }) }) @@ -91,26 +97,39 @@ describe('UseApi with custom instance', () => { }) }) -// describe('ApiClientProvider wrapper', () => { -// test('should handle successful Api call with ApiClientPeovider wrapper', async () => { -// const mockedClient = useApiClient as jest.MockedFunction -// const hook = new MockAdapter(api) -// mockedClient.mockReturnValue(api) -// mock.onPost('/provider').reply(200, postResponse) -// const { result } = renderHook(() => useApi(), { -// wrapper: ({ children }) => {children}, -// }) -// act(() => { -// result.current.mutate('/submit', formdata) -// }) -// expect(result.current.fetching).toBe(true) -// await waitFor(() => { -// expect(result.current.success).toBe(true) -// expect(result.current.data).toEqual(postResponse) -// expect(result.current.error).toBe(null) -// }) -// }) -// }) +describe('ApiClientProvider wrapper', () => { + test('should handle successful Api call with ApiClientProvider wrapper', async () => { + const mockedClient = useApiClient as jest.MockedFunction + mockedClient.mockReturnValue(api) + mock.onPost('/provider', formdata).reply(200, postResponse) + const { result } = renderHook(() => useApi(), { + wrapper: ({ children }) => ( + {children} + ), + }) + act(() => { + result.current.mutate('/provider', formdata) + }) + expect(result.current.fetching).toBe(true) + await waitFor(() => { + checkSuccess(result) + }) + }) +}) + +describe('Without Provider and instance config, raw call', () => { + test('should handle successful Api call without Provide and instance config', async () => { + mock.onPost('/raw', formdata).reply(200, postResponse) + const { result } = renderHook(() => useApi()) + act(() => { + result.current.mutate('/raw', formdata) + }) + expect(result.current.fetching).toBe(true) + await waitFor(() => { + checkSuccess(result) + }) + }) +}) describe('UseFormApi hook', () => { test('should handle successful Api call with submit form ', async () => { @@ -122,9 +141,7 @@ describe('UseFormApi hook', () => { }) expect(result.current.fetching).toBe(true) await waitFor(() => { - expect(result.current.success).toBe(true) - expect(result.current.data).toEqual(postResponse) - expect(result.current.error).toBe(null) + checkSuccess(result) }) }) }) @@ -136,3 +153,29 @@ describe('asyncApi check', () => { expect(data).toEqual(postResponse) }) }) + +describe('Without Provider and with instance config,', () => { + test('should handle successful Api call without Provide and with instance config', async () => { + mock = new MockAdapter(axios) + mock.onPost('/raw', formdata).reply(200, postResponse) + const { result } = renderHook(() => + useApi({ + baseURL: 'api.com', + headers: { test: 'test', 'Content-Type': 'multiprt/formdata' }, + }) + ) + act(() => { + result.current.mutate('/raw', formdata) + }) + expect(result.current.fetching).toBe(true) + await waitFor(() => { + checkSuccess(result) + }) + }) +}) + +function checkSuccess(result: Result, response: ResponseType = postResponse) { + expect(result.current.success).toBe(true) + expect(result.current.data).toEqual(response) + expect(result.current.error).toBe(null) +} From c29d214ff9267d22299a32fdbdbee5eee33ee288 Mon Sep 17 00:00:00 2001 From: Sainul Abid <102832096+abidta@users.noreply.github.com> Date: Sat, 1 Jun 2024 13:26:01 +0530 Subject: [PATCH 3/3] Update README.md v0.3.0 --- README.md | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index db07b77..53c49b7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@   [![Build status](https://img.shields.io/github/actions/workflow/status/abidta/useipa/CI.yml?branch=main&label=CI&logo=github&style=flat-square)](https://github.com/abidta/useipa/actions/workflows/ci.yml) -# useipa Package + # useipa Package ### A Powerful and Lightweight React Hook for Managing API Calls and Forms @@ -20,6 +20,33 @@ yarn add useipa ## Usage +### `ApiClientProvider` + +Setting Up `ApiClientProvider` +First, import the necessary components and set up your ApiClientProvider with the desired configuration.Within any child component, you can use the [useApi](#useapi-the-core-hook) hook to access the configured Axios instance. + +```tsx +import React from 'react'; +import { ApiClientProvider } from 'useipa'; + +const apiClientConfig = { + baseURL: 'https://api.example.com', + headers: { + Authorization: 'Bearer YOUR_ACCESS_TOKEN', + }, +}; + +const App = () => { + return ( + + + + ); +}; + +export default App; +``` + ### `useApi` The Core Hook The `useApi` hook provides methods for fetching and mutating data from an API. @@ -88,6 +115,68 @@ const MutateComponent = () => { export default MutateComponent ``` +The [ApiClientProvider](#apiclientprovider) allows you to configure and manage a centralized Axios instance for all your API requests. If no configuration is provided, a default instance is used. Alternatively, you can explicitly pass an `instanceConfig` argument to the [`useApi`](#useapi-the-core-hook) hook to override the default or context-provided instance. +Example: +```tsx +import React from 'react'; +import { ApiClientProvider, useApi } from 'useipa'; +import axios from 'axios'; + +const apiClientConfig = { + baseURL: 'https://api.example.com', + headers: { + Authorization: 'Bearer YOUR_ACCESS_TOKEN', + }, +}; + +const App = () => { + return ( + + + + + ); +}; + +const YourComponent = () => { + const { fetching, data, error, mutate } = useApi(); + + const fetchData = async () => { + await mutate('/default-endpoint', { method: 'GET' }); + }; + + React.useEffect(() => { + fetchData(); + }, []); + + if (fetching) return
Loading...
; + if (error) return
Error: {error.message}
; + + return
Data: {JSON.stringify(data)}
; +}; + +const AnotherComponent = () => { + const customConfig = { baseURL: 'https://custom-api.example.com' }; + // override ApiClientProvider Instance + const { fetching, data, error, mutate } = useApi(customConfig); + + const fetchData = async () => { + await mutate('/custom-endpoint', { method: 'GET' }); + }; + + React.useEffect(() => { + fetchData(); + }, []); + + if (fetching) return
Loading...
; + if (error) return
Error: {error.message}
; + + return
Data: {JSON.stringify(data)}
; +}; + +export default App; +``` + ### `useFormApi` Hook The `useFormApi` hook is designed for handling form submissions. @@ -195,8 +284,9 @@ export default AsyncComponent - `fetching`: A boolean indicating if the request is in progress. - `error`: An error object if the request fails. - `success`: A boolean indicating if the request was successful. - - **Methods:** +- **Parameters:** + - `instanceConfig?` : AxiosInstanceConfig +- **Methods:** - `fetchData(endpoint, config?)`: Fetches data from an API endpoint. - `endpoint`: The URL of the API endpoint. - `config `(optional): An object containing configuration options like headers or method (defaults to `GET`).
@@ -215,7 +305,8 @@ export default AsyncComponent ### `useFormApi` - **State and Methods:** Inherits all state and methods from `useApi`. - +- **Parameters:** + - `instanceConfig?` : AxiosInstanceConfig - **Methods:** - `submitForm(endpoint: string, inputData: object,config?)`: Submit form data to the specified endpoint using the POST method. @@ -248,6 +339,9 @@ fetchData('/api/data', config) `useApi` logs errors to the console by default. You can implement custom error handling in your component. The `error` property in `useApi` and `useFormApi` stores the error object for programmatic handling. +## Contributing +If you encounter any issues or have suggestions for improvements, please open an issue or submit a pull request on [GitHub](https://github.com/abidta/useipa). + ## Conclusion The `useipa` package simplifies API interactions in React applications, providing a robust and easy-to-use set of hooks for data fetching and form submission. For more detailed usage and advanced configurations, refer to the provided examples and API reference.