Skip to content

Instantly share code, notes, and snippets.

@scorsi
Created February 27, 2020 15:18
Show Gist options
  • Save scorsi/e879b6aa4bf673d3845dfeebcc04b1a1 to your computer and use it in GitHub Desktop.
Save scorsi/e879b6aa4bf673d3845dfeebcc04b1a1 to your computer and use it in GitHub Desktop.
A Fetch hook with super-powers, lightweight but very performant
// eslint-disable-next-line import/no-mutable-exports,prefer-const
export let baseUrl = process.env.REACT_APP_API_DOMAIN;
export const constructUrl = (path, queryParams = null) => {
const _url = new URL(baseUrl);
_url.pathname = path;
if (queryParams) _url.search = new URLSearchParams(queryParams).toString();
return _url.toString();
};
// eslint-disable-next-line import/no-mutable-exports,prefer-const
export let defaultFetchOptions = {
method: 'GET',
mode: 'cors',
credentials: 'include',
cache: 'no-cache',
};
export const doFetch = (url, options) => fetch(url, { ...defaultFetchOptions, ...options })
.then((response) => {
if (!response.ok) {
return Promise.reject(response);
}
return response.json()
.then((data) => Promise.resolve({ ...data }));
})
.catch((error) => {
if (['PromiseCanceledError', 'AbortError'].includes(error.name)) {
return Promise.reject(error.name);
}
// /!\ WARNING DOMAIN-SPECIFIC CODE
// My api is always retourning a error name in json
// This should be adapted for your use-case
if (error instanceof Response) {
return error.json()
.then((data) => Promise.reject(data.error));
}
return Promise.reject('unknown_error');
});
import {
useEffect, useMemo, useRef, useState,
} from 'react';
import useCancellablePromise from './useCancellablePromise';
import { constructUrl, defaultFetchOptions, doFetch } from './fetch';
export default (
path,
options,
defaultData = null,
) => {
const {
transformData, queryParams, bodyParams,
method, mode, credentials,
} = options;
const { cancellablePromise } = useCancellablePromise();
const [isLoading, setIsLoading] = useState(true);
const [newData, setNewData] = useState(defaultData);
const [data, setData] = useState(defaultData);
const [error, setError] = useState(null);
const controller = useRef(null);
const memoizedUrl = useMemo(() => constructUrl(path, queryParams), [path, queryParams]);
const memoizedFetchOptions = useMemo(() => ({
method: method || defaultFetchOptions.method,
mode: mode || defaultFetchOptions.mode,
credentials: credentials || defaultFetchOptions.credentials,
body: JSON.stringify(bodyParams) || defaultFetchOptions.body,
}), [method, mode, credentials, bodyParams]);
useEffect(() => {
if (controller.current !== null) controller.current.abort();
controller.current = new AbortController();
setIsLoading(true);
cancellablePromise(
doFetch(memoizedUrl, {
...memoizedFetchOptions,
signal: controller.current.signal,
}),
controller.current,
)
.then((_data) => {
setNewData(_data);
})
.catch((_error) => {
if (
(_error instanceof String && ['PromiseCanceledError', 'AbortError'].includes(_error))
|| (_error instanceof Object && _error.name === 'PromiseCanceledError')
) {
return;
}
setIsLoading(false);
setNewData(null);
setError(_error);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setIsLoading, setNewData, memoizedUrl, memoizedFetchOptions]);
useEffect(() => {
if (transformData) {
setData((oldData) => transformData(newData, oldData));
} else {
setData(newData);
}
setIsLoading(false);
}, [newData, setData, transformData]);
return {
data,
error,
isLoading,
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment