Skip to content

Instantly share code, notes, and snippets.

@cosemansp
Created August 26, 2022 12:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cosemansp/8f27324993886b71106faec6e58082c1 to your computer and use it in GitHub Desktop.
Save cosemansp/8f27324993886b71106faec6e58082c1 to your computer and use it in GitHub Desktop.
/* 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);
});
});
/* 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