Created
August 26, 2022 12:03
-
-
Save cosemansp/8f27324993886b71106faec6e58082c1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @typescript-eslint/no-explicit-any */ | |
import { waitFor, renderHook } from '@testing-library/react'; | |
import { AxiosError } from 'axios'; | |
import nock from 'nock'; | |
import useQuery from './useQuery'; | |
// mock useAuth | |
vi.mock('./useAuth', () => ({ | |
default: vi.fn(() => ({ | |
isAuthenticated: true, | |
acquireTokenSilent: vi.fn().mockResolvedValue({ idToken: '1234567890' }), | |
})), | |
})); | |
describe('useQuery', () => { | |
let testData: any; | |
beforeEach(() => { | |
testData = { | |
id: 123, | |
name: 'john', | |
}; | |
}); | |
test('simple query', async () => { | |
// arrange | |
nock('http://localhost:8080').get('/api/user').reply(200, testData); | |
// act | |
const { result } = renderHook(() => useQuery('/api/user')); | |
// assert | |
const previousResult = result.current; | |
await waitFor(() => { | |
expect(result.current.isLoading).not.toBe(previousResult.isLoading); | |
}); | |
expect(result.current.isLoading).toBe(false); | |
expect(result.current.data).toEqual(testData); | |
}); | |
test('query function', async () => { | |
// arrange | |
nock('http://localhost:8080').get('/api/user').reply(200, testData); | |
// act | |
const { result } = renderHook(() => | |
useQuery(async (api) => { | |
const res = await api.get('/api/user'); | |
return res.data; | |
}), | |
); | |
// assert | |
const previousResult = result.current; | |
await waitFor(() => { | |
expect(result.current.isLoading).not.toBe(previousResult.isLoading); | |
}); | |
expect(result.current.isLoading).toBe(false); | |
expect(result.current.data).toEqual(testData); | |
}); | |
test('params are placed on request', async () => { | |
// arrange | |
nock('http://localhost:8080').get('/api/user?id=1').reply(200, testData); | |
// act | |
const { result } = renderHook(() => useQuery('/api/user', { params: { id: 1 } })); | |
// assert | |
const previousResult = result.current; | |
await waitFor(() => { | |
expect(result.current.isLoading).not.toBe(previousResult.isLoading); | |
}); | |
expect(result.current.error).toBeUndefined(); | |
}); | |
test('handle error', async () => { | |
// arrange | |
// response with not found (404) | |
nock('http://localhost:8080').get('/api/user').reply(404); | |
// act | |
const { result } = renderHook(() => useQuery('/api/user')); | |
// assert | |
const previousResult = result.current; | |
await waitFor(() => { | |
expect(result.current.isLoading).not.toBe(previousResult.isLoading); | |
}); | |
expect(result.current.error).toBeDefined(); | |
expect((result.current.error as Error).message).toMatch(`404`); | |
expect((result.current.error as AxiosError).code).toBe('ERR_BAD_REQUEST'); | |
expect((result.current.error as AxiosError).response?.status).toBe(404); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable react-hooks/exhaustive-deps */ | |
import * as React from 'react'; | |
import useApi, { ApiInstance } from './useApi'; | |
import { useMount } from './useMount'; | |
type SearchParams = { | |
[key: string]: number | string | boolean | Date; | |
}; | |
type QueryOptions = { | |
enabled?: boolean; | |
params?: SearchParams; | |
}; | |
type QueryFn<TData> = (api: ApiInstance<TData>) => Promise<TData>; | |
/** | |
* Usage | |
* | |
* const { data: user } = useQuery(`/api/users/${id}`); | |
* const { data: user } = useQuery(`/api/users`, { | |
* params: { | |
* id: '123' | |
* } | |
* }); | |
* const { data: orders } = useQuery(`/api/orders`, { | |
* params: { | |
* filter: user.name | |
* }, | |
* enabled: !!user, | |
* }); | |
* | |
* const { data: orders } = useQuery((api) => { | |
* const res = api.get(`/api/users/${id}`)); | |
* return res.data; | |
* }); | |
*/ | |
const useQuery = <TData, TError extends Error = Error>( | |
urlOrQueryFn: string | QueryFn<TData>, | |
options: QueryOptions = {}, | |
) => { | |
const api = useApi(); | |
const [data, setData] = React.useState<TData | undefined>(); | |
const [error, setError] = React.useState<TError | undefined>(undefined); | |
const [isLoading, setLoading] = React.useState(false); | |
const defaultedOptions = React.useMemo(() => ({ ...options, enabled: true }), [options]); | |
const controllerRef = React.useRef(new AbortController()); | |
const fetchData = React.useCallback( | |
async (throwOnError = false) => { | |
setLoading(true); | |
let requestPromise: Promise<TData>; | |
if (typeof urlOrQueryFn === 'string') { | |
requestPromise = api | |
.get<TData>(urlOrQueryFn, { | |
params: defaultedOptions.params, | |
signal: controllerRef.current.signal, // to abort the request | |
}) | |
.then((result) => result.data); | |
} else { | |
requestPromise = urlOrQueryFn(api); | |
} | |
return requestPromise | |
.then((responseData) => { | |
setData(responseData); | |
return responseData; | |
}) | |
.catch((err) => { | |
setError(err); | |
if (throwOnError) { | |
throw err; | |
} | |
}) | |
.finally(() => { | |
setLoading(false); | |
}); | |
}, | |
[urlOrQueryFn, defaultedOptions, api], | |
); | |
useMount(() => { | |
if (defaultedOptions.enabled) { | |
fetchData(false); | |
} | |
return () => { | |
// abort the pending request when unmounting | |
controllerRef.current.abort(); | |
}; | |
}); | |
return { data, error, isLoading, refetch: fetchData }; | |
}; | |
export default useQuery; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment