I am coming from Angular and relatively new to React. I wrote a custom hook to fetch data that is working fine but I can’t test the state exposed by the hook. Specifically, I want to test that:
- The loading state is false before fetching: Working!
- The loading state is true while fetching: 👈 Problem! I never get
true
- The loading state is false after fetching: I think it is working, but I can’t be sure until I get a truthy state before.
I am struggling to find a way to test this kind of async state inside a hook. Any help is more than appreciated!
// useFetch.ts
import { useCallback, useEffect, useState } from 'react';
interface Props{
endpoint: string,
}
interface FetchReturn<T> {
result: T | null,
loading: boolean,
fetchLazy: (params?: any) => Promise<T | null>,
statusCode: number,
error: any,
};
export function useFetch<T>({ endpoint, method }: Props): FetchReturn<T> {
const [result, setResult] = useState<T | null>(null);
const [error, setError] = useState<any>(null);
const [loading, setLoading] = useState(false);
const fetchData = useCallback((_data: any): Promise<T | null> => {
setLoading(true);
setError(null);
setResult(null);
const options: any = {
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
method: method,
};
// Mostly in case of POST, PUT, PATCH & DELETE
if (method !== 'GET' && _data !== undefined) {
options.headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(_data);
}
return new Promise(async (resolve, reject) => {
setLoading(true);
const response = await fetch(endpoint, options);
try {
const text = await response.text();
const data = JSON.parse(text);
if (!response.ok) {
setError(data?.errors?.json);
reject(error);
} else {
setResult(data);
setError(null);
setLoading(false);
resolve(data);
}
} catch (err: any) {
setResult(null);
setError(err);
setLoading(false);
reject(err);
}
})
},[endpoint, method]);
const fetchLazy = (params = {}): Promise<T | null> => {
return fetchData(params);
}
return ({ result, loading, fetchLazy, statusCode, error });
}
I use it as a lazy method, so I can trigger request with arbitrary actions:
const myComp: () => {
const { loading, fetchLazy } = useFetch({ endpoint: myEndpoint });
await fetchLazy({ some: 'options' })
.then((result) => {/* handle result */})
.catch((error) => {/* handle error */});
return <>loading: {loading}</>
}
And this is the test:
// useFetch.test.spec
import fetchMock, { enableFetchMocks } from 'jest-fetch-mock';
import { beforeEach, describe, expect, test, jest } from '@jest/globals';
import { act, renderHook, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import { useFetch } from './useFetch';
enableFetchMocks();
describe.only('useFetch', () => {
beforeEach(() => {
fetchMock.resetMocks();
jest.spyOn(global, 'fetch');
});
test.only('> fetchLazy should update loading state', async () => {
const answer = { 'POST works': true };
fetchMock.mockResponseOnce(JSON.stringify(answer));
const options = { endpoint: '/', method: 'POST', csrfToken: '0' };
const { result: { current: hook }, rerender } = renderHook(() => useFetch(options as any));
expect(hook.loading).toBe(false);
await act(async () => {
await hook.fetchLazy({ 'post': 'works?' });
// neither this…
expect(hook.loading).toBe(true);
});
// …nor this works.
expect(hook.loading).toBe(true);
rerender();
expect(hook.loading).toBe(false);
});
});