Skip to content

Instantly share code, notes, and snippets.

@cosemansp
Created August 26, 2022 12:04
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/7715f8ddae304566ec3403bba9a95d70 to your computer and use it in GitHub Desktop.
Save cosemansp/7715f8ddae304566ec3403bba9a95d70 to your computer and use it in GitHub Desktop.
import * as React from 'react';
import useApi from './useApi';
import { useMount } from './useMount';
type MutationOptions<TData, TError> = {
onError?: (err: TError, config: MutationConfig) => void;
onSuccess?: (data: TData, config: MutationConfig) => void;
};
type MutationConfig = {
data?: unknown;
params?: Record<string, number | string | boolean | Date>;
};
/**
* Usage
*
* // simple mutation
* const api = useApi();
* api.post('/api/user', { name: 'john' });
*
* // handling mutation state
* const { mutate as addUser, data, error, isLoading } = useMutation('PUT', '/api/users');
*
* addUser({
* data: { name: 'john' },
* params: { id: 123 }
* });
*
* // responding to mutation state
* // and result action
* const { mutate, error, isLoading } = useMutation('POST', '/api/users', {
* onSuccess(data) {
* # refresh useQuery
* refetch();
* }
* });
* mutate({
* data: { name: 'john' }
* });
*
*/
const useMutation = <TData, TError = Error>(
method: 'POST' | 'PUT' | 'PATCH' | 'DELETE',
url: string,
options: MutationOptions<TData, TError> = {},
) => {
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 controllerRef = React.useRef(new AbortController());
const mutateFn = React.useCallback(
async (config: MutationConfig = {}): Promise<TData | void> => {
setLoading(true);
return api
.request<TData>({
url,
method,
data: config.data,
signal: controllerRef.current.signal, // to abort the request
params: {
...(config.params ? config.params : {}),
},
})
.then((result) => {
setData(result.data);
if (options.onSuccess) {
options.onSuccess(result.data, config);
}
return result.data;
})
.catch((err) => {
setError(err);
if (options.onError) {
options.onError(err, config);
}
})
.finally(() => {
setLoading(false);
});
},
[url, api, options, method],
);
useMount(() => () => {
// abort the pending request when unmounting
controllerRef.current.abort();
});
return { data, error, isLoading, mutate: mutateFn };
};
export default useMutation;
import { waitFor, renderHook } from '@testing-library/react';
import nock from 'nock';
import { act } from 'react-dom/test-utils';
import useMutation from './useMutation';
describe('useMutation', () => {
test('post data & handle response', async () => {
// arrange
const testData = {
id: 123,
name: 'john',
};
type ResponseData = typeof testData;
nock('http://localhost:8080').post('/api/user').reply(200, testData);
// act
const { result } = renderHook(() =>
useMutation<ResponseData>('POST', '/api/user', {
onSuccess: (data) => {
expect(data).toEqual(testData);
},
}),
);
// call mutate to trigger the post/put/delete request
act(() => {
result.current.mutate({
data: {
name: 'john',
},
});
});
// assert
const previousResult = result.current;
await waitFor(() => {
expect(result.current).not.toBe(previousResult);
});
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toEqual(testData);
});
test('post data and params', async () => {
// arrange
const testData = {
id: 123,
name: 'john',
};
type ResponseData = typeof testData;
nock('http://localhost:8080')
.put('/api/user')
.query({
id: 123,
})
.reply(200, testData);
// act
const { result } = renderHook(() => useMutation<ResponseData>('PUT', '/api/user'));
// call mutate to trigger the post/put/delete request
act(() => {
result.current.mutate({
data: {
name: 'john',
},
params: {
id: 123,
},
});
});
// assert
const previousResult = result.current;
await waitFor(() => {
expect(result.current).not.toBe(previousResult);
});
expect(result.current.error).toBeUndefined();
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toEqual(testData);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment