@pinkairship/use-data-fetch
Version:
A data fetch hook that stays out of your way.
529 lines (483 loc) • 15.3 kB
JavaScript
;
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;