UNPKG

@pinkairship/use-data-fetch

Version:

A data fetch hook that stays out of your way.

529 lines (483 loc) 15.3 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var React = require('react'); var axios = require('axios'); var LruCache = require('lru-cache'); function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; } var React__default = /*#__PURE__*/_interopDefault(React); var axios__default = /*#__PURE__*/_interopDefault(axios); var LruCache__default = /*#__PURE__*/_interopDefault(LruCache); const DataFetchContext = React__default["default"].createContext({ dataFetchInstance: null, screenReaderAlert: () => {} }); // stable function signature for equality checks when updateStateHook is not defined function noop$1() {} function setupAxios(axiosCreateOpts) { const axiosInstance = axios__default["default"].create(axiosCreateOpts); return axiosInstance; } function DataFetchProvider({ children, dataFetchInstance = null, axiosCreateOpts = {}, screenReaderAlert = () => {}, makeMockDataFetchInstance = null, useCache = false, cacheSize = 50, updateStateHook = noop$1, debugCache = false }) { if (makeMockDataFetchInstance && dataFetchInstance) { throw 'Cannot use `makeMockDataFetchInstance` and `dataFetchInstance` together.'; } if (dataFetchInstance == null) { dataFetchInstance = setupAxios(axiosCreateOpts); if (makeMockDataFetchInstance) { makeMockDataFetchInstance(dataFetchInstance); } } const cache = new LruCache__default["default"](cacheSize); if (debugCache && window) { if (!window.dataFetchCaches) { window.dataFetchCaches = []; } window.dataFetchCaches.push(cache); } const contextValue = { dataFetchInstance, screenReaderAlert, cache, useCache, updateStateHook }; return React__default["default"].createElement(DataFetchContext.Provider, { value: contextValue }, children); } function noop() {} function useDataFetch(path, { updateStateHook: hookUpdateStateHook, alertScreenReaderWith: alertScreenReaderWith, requestConfig: hookRequestConfig, useCache: hookUseCache, requestStateListener: hookRequestStateListener } = { alertScreenReaderWith: undefined, hookRequestConfig: {}, hookUseCache: undefined, hookRequestStateListener: noop }) { const { dataFetchInstance, screenReaderAlert, cache, useCache: contextUseCache, updateStateHook: providerUpdateStateHook } = React.useContext(DataFetchContext); function makeRequest(method, data, { methodUseCache, methodRequestConfig: methodRequestConfig, methodUpdateStateHook: methodUpdateStateHook, requestStateListener: methodRequestStateListener } = { methodRequestConfig: {}, methodUpdateStateHook: undefined }) { const updateStateHook = typeof hookUpdateStateHook != 'undefined' ? hookUpdateStateHook : providerUpdateStateHook; const hookToUpdateState = typeof methodUpdateStateHook != 'undefined' ? methodUpdateStateHook : updateStateHook; // Order of precedence: method use - method defined - context defined let computedUseCache = (typeof hookUseCache != 'undefined' ? hookUseCache : contextUseCache) && method.toLowerCase() == 'get'; computedUseCache = typeof methodUseCache != 'undefined' ? methodUseCache : computedUseCache; const requestStateListener = typeof methodRequestStateListener != 'undefined' ? methodRequestStateListener : hookRequestStateListener || noop; let promise; if (computedUseCache) { const cachedValue = cache.get(path); if (cachedValue) { requestStateListener('success'); promise = Promise.resolve(cachedValue); } } if (!promise) { // don't overwrite data from requestConfig unless it is present. const requestData = data ? { data } : {}; const finalRequestConfig = { method, url: path, ...hookRequestConfig, ...methodRequestConfig, ...requestData }; requestStateListener('running'); promise = dataFetchInstance(finalRequestConfig); } return promise.then(responseData => { if (responseData.config.signal?.aborted) return responseData; requestStateListener('success'); if (computedUseCache) cache.set(path, responseData); return responseData; }).then(responseData => { if (responseData.config.signal?.aborted) return responseData; if (hookToUpdateState) hookToUpdateState(responseData); return responseData; }).then(responseData => { if (alertScreenReaderWith && !responseData.config.signal?.aborted) screenReaderAlert(alertScreenReaderWith); return responseData; }).catch(error => { requestStateListener('error'); return error; }); } const get = React.useMemo(() => (data, { useCache, requestConfig, requestStateListener, updateStateHook } = {}) => makeRequest('get', data, { methodUseCache: useCache, methodUpdateStateHook: updateStateHook, methodRequestConfig: requestConfig, requestStateListener }), // eslint-disable-next-line react-hooks/exhaustive-deps [hookUpdateStateHook]); const query = React.useMemo(() => (params, { useCache, requestConfig, requestStateListener, updateStateHook } = {}) => makeRequest('get', undefined, { methodUseCache: useCache, methodUpdateStateHook: updateStateHook, methodRequestConfig: { ...requestConfig, params }, requestStateListener }), // eslint-disable-next-line react-hooks/exhaustive-deps [hookUpdateStateHook]); const post = React.useMemo(() => (data, { useCache, requestConfig, requestStateListener, updateStateHook } = {}) => { return makeRequest('post', data, { methodUseCache: useCache, methodUpdateStateHook: updateStateHook, methodRequestConfig: requestConfig, requestStateListener }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [hookUpdateStateHook]); const put = React.useMemo(() => (data, { useCache, requestConfig, requestStateListener, updateStateHook } = {}) => makeRequest('put', data, { methodUseCache: useCache, methodUpdateStateHook: updateStateHook, methodRequestConfig: requestConfig, requestStateListener }), // eslint-disable-next-line react-hooks/exhaustive-deps [hookUpdateStateHook]); const patch = React.useMemo(() => (data, { useCache, requestConfig, requestStateListener, updateStateHook } = {}) => makeRequest('patch', data, { methodUseCache: useCache, methodUpdateStateHook: updateStateHook, methodRequestConfig: requestConfig, requestStateListener }), // eslint-disable-next-line react-hooks/exhaustive-deps [hookUpdateStateHook]); const destroy = React.useMemo(() => (data, { useCache, requestConfig, requestStateListener, updateStateHook } = {}) => makeRequest('delete', data, { methodUseCache: useCache, methodUpdateStateHook: updateStateHook, methodRequestConfig: requestConfig, requestStateListener }), // eslint-disable-next-line react-hooks/exhaustive-deps [hookUpdateStateHook]); const request = React.useMemo(() => (data, { useCache, requestConfig, requestStateListener, updateStateHook } = {}) => { const mergedConfig = { ...hookRequestConfig, ...requestConfig }; if (!hookRequestConfig.url) { throw 'Request must have url set.'; } if (!hookRequestConfig.method) { throw 'Request must have a method set.'; } return makeRequest('request', data, { methodUseCache: useCache, methodUpdateStateHook: updateStateHook, methodRequestConfig: mergedConfig, requestStateListener }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [hookUpdateStateHook]); return { get, post, put, patch, destroy, request, query }; } function useFetchOnMount(path, { onSuccess: onSuccess, onFailure: onFailure, hookOptions: hookOptions, cancelRequestOnUnmount: cancelRequestOnUnmount } = { onSuccess: request => request, onFailure: request => request, hookOptions: {}, cancelRequestOnUnmount: false }) { const [controller] = React.useState(new AbortController()); const [renders, setRenders] = React.useState(0); const dataFetch = useDataFetch(path, { ...hookOptions }); React.useEffect(() => { dataFetch.get(undefined, { requestConfig: { signal: controller.signal } }).then(req => { if (!(cancelRequestOnUnmount && req.config.signal.aborted)) { onSuccess(req); } return req; }).catch(error => { if (!cancelRequestOnUnmount) { onFailure(error); } return error; }); // eslint-disable-next-line react-hooks/exhaustive-deps return () => { if (cancelRequestOnUnmount) { controller.abort(); } }; }, []); // Must reset the useEffect so that a refetch will update the state of the updateStateHook // when the get is applied again (possibly in a refetch) const fetches = {}; Object.keys(dataFetch).forEach(df => { fetches[df] = (newPath, opts) => { setRenders(renders + 1); return dataFetch[df](newPath, opts); }; }); return fetches; } const passThrough$1 = data => data; const getArray = data => { if (Array.isArray(data)) { return data; } if (Object.keys(data).length == 1) { const arr = data[Object.keys(data)[0]]; if (Array.isArray(arr)) { return data[Object.keys(data)[0]]; } } throw new Error('Your data does not have a recognized pattern. Please provide your own transform function.'); }; function getKey(data) { if (!data.id) { throw new Error('Cannot use default extractObjectKey for objects that do not have an id field.'); } return data.id; } function useFetchedArray(path, { onSuccess: onSuccess, onFailure: onFailure, hookOptions: hookOptions, transform: transform, extractList: extractList, replaceValue: replaceValue, removeValue: removeValue, updatesUsePath: updatesUsePath, extractObjectKey: extractObjectKey, cancelRequestOnUnmount: cancelRequestOnUnmount } = { onSuccess: passThrough$1, onFailure: passThrough$1, hookOptions: {}, transform: passThrough$1, extractList: getArray, replaceValue: null, removeValue: null, updatesUsePath: null, extractObjectKey: getKey, cancelRequestOnUnmount: false }) { // TODO: the defaults for the above are not set when built, figure it out // and these can be removed extractList = extractList ? extractList : getArray; onSuccess = onSuccess ? onSuccess : passThrough$1; onFailure = onFailure ? onFailure : passThrough$1; transform = transform ? transform : passThrough$1; extractObjectKey = extractObjectKey ? extractObjectKey : getKey; ////// const [values, setValues] = React.useState([]); const [requestState, setRequestState] = React.useState('pending'); const updateStateHook = React.useCallback(({ data }) => { const newValues = extractList(data); setValues(newValues); }, [extractList]); const dataFetch = useFetchOnMount(path, { onSuccess: onSuccess, onFailure: onFailure, hookOptions: { ...hookOptions, requestStateListener: setRequestState, updateStateHook }, cancelRequestOnUnmount: cancelRequestOnUnmount }); const updateValue = replaceValue ? replaceValue : ({ data }) => { const newData = transform(data); const objectKey = extractObjectKey(newData); setValues(state => { const oldValueIndex = state.findIndex(v => v.id == objectKey); const newValues = [...state]; newValues[oldValueIndex] = newData; return newValues; }); }; const updatePath = updatesUsePath ? updatesUsePath(path) : data => `${path}/${data.id}`; const managedDataFetch = { get: dataFetch.get, post: (postData, opts = {}) => { const postUpdateStateHook = ({ data }) => { let newValues = transform(data); newValues = Array.isArray(newValues) ? newValues : [newValues]; setValues(state => [...state, ...newValues]); }; return dataFetch.post(postData, { updateStateHook: postUpdateStateHook, ...opts }); }, put: (putData, opts = {}) => { return dataFetch.put(putData, { updateStateHook: updateValue, requestConfig: { url: updatePath(putData) }, ...opts }); }, patch: (patchData, opts = {}) => { return dataFetch.patch(patchData, { updateStateHook: updateValue, requestConfig: { url: updatePath(patchData) }, ...opts }); }, destroy: (destroyData, removalKeys, opts = {}) => { const removeKeyValue = removalKey => { setValues(state => { const valueIndex = state.findIndex(j => j.id === removalKey); const newValues = [...state]; newValues.splice(valueIndex, 1); return newValues; }); }; const destroyUpdateStateHook = removeValue ? removeValue : () => { if (removalKeys != undefined && Array.isArray(removalKeys)) { removalKeys.forEach(removalKey => { removeKeyValue(removalKey); }); } else { removeKeyValue(removalKeys); } }; return dataFetch.destroy(destroyData, { updateStateHook: destroyUpdateStateHook, requestConfig: { url: updatePath(destroyData) }, ...opts }); } }; return [values, setValues, requestState, managedDataFetch]; } const passThrough = data => data; function useFetched(path, { onSuccess: onSuccess, onFailure: onFailure, hookOptions: hookOptions, createUsesPath: createUsesPath, cancelRequestOnUnmount: cancelRequestOnUnmount } = { onSuccess: passThrough, onFailure: passThrough, hookOptions: {}, createUsesPath: null, cancelRequestOnUnmount: false }) { onSuccess = onSuccess ? onSuccess : passThrough; onFailure = onFailure ? onFailure : passThrough; const [value, setValue] = React.useState({}); const [requestState, setRequestState] = React.useState('pending'); const updateStateHook = React.useCallback(({ data }) => { setValue(data); }, []); const dataFetch = useFetchOnMount(path, { onSuccess: onSuccess, onFailure: onFailure, hookOptions: { ...hookOptions, requestStateListener: setRequestState, updateStateHook }, cancelRequestOnUnmount: cancelRequestOnUnmount }); const createPath = createUsesPath ? createUsesPath(path) : () => { const parts = path.split('/'); return parts.slice(0, parts.length - 1).join('/'); }; const managedDataFetch = { get: dataFetch.get, post: (postData, opts = {}) => { return dataFetch.post(postData, { ...opts, requestConfig: { url: createPath() } }); }, put: dataFetch.put, patch: dataFetch.patch, destroy: dataFetch.destroy }; return [value, setValue, requestState, managedDataFetch]; } exports.DataFetchProvider = DataFetchProvider; exports.useDataFetch = useDataFetch; exports.useFetchOnMount = useFetchOnMount; exports.useFetched = useFetched; exports.useFetchedArray = useFetchedArray;