/** @module useData */
import { useState, useEffect } from 'react';
import axios from 'axios';
import axiosRetry from 'axios-retry';
import { functionOrDefault } from '../helpers';
// Use as persistent store to keep track of the state of the data and prevent refetching
const CACHE = {};
/**
* Layout fetching hook that assumes the default layout endpoint
* @function
* @param {string} type - object type for standard get layout endpoint
* @param {string} key - layout key for standard get layout endpoint
* @param {string} url - optional if you are not using the standard endpoint
* @param {object} existingLayout - optional if you already have the layout and do not want to fetch it
* @returns [object, boolean] - layout, loading
*/
export const useLayout = (type, key, url = null, existingLayout = null) => {
const fetchUrl = url || `/api/layout/get?objectType=${type}&layoutKey=${key}`;
// If existingLayout is not null, we will set the flag to true to prevent fetching
const [data, isLoading] = useStaleData(fetchUrl, existingLayout || {}, existingLayout !== null);
return [data, isLoading];
};
/**
* Hook to fetch map config by key or cache
* @function
* @returns {Array<object|boolean>} mapConfig, mapConfigLoading
*/
export const useMapConfig = (map_key) => {
const url = `/api/map/config/getForMap/${map_key}`;
// If debug mode is enabled, we will use the fake data
const defaultValue = [];
const [data, isLoading ] = useStaleData(url, defaultValue, false);
const [mapConfig, setMapConfig] = useState([]);
const [mapConfigLoading, setMapConfigLoading] = useState(true);
// Monitor the loading state of the data and update this hook's state
// This prevents the component using this hook from having to handle the parsing itself
// We do not need to clean up the useEffect as the staleData hook does it for us
useEffect(() => {
if (!isLoading) {
setMapConfig(data);
setMapConfigLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading]);
return [mapConfig, mapConfigLoading];
};
/**
* Hook to fetch config by key or cache
* @function useConfig
* @param {string} config_key
* @returns {Array<object|boolean>} config, loading
*/
export const useConfig = (config_key) => {
const url = `/api/app/config/get?configKey=${config_key}`;
// If debug mode is enabled, we will use the fake data
const defaultValue = {};
const [data, isLoading ] = useStaleData(url, defaultValue, false);
const [config, setConfig] = useState([]);
const [configLoading, setConfigLoading] = useState(true);
// Monitor the loading state of the data and update this hook's state
// This prevents the component using this hook from having to handle the parsing itself
// We do not need to clean up the useEffect as the staleData hook does it for us
useEffect(() => {
if (!isLoading) {
setConfig(data);
setConfigLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading]);
return [config, configLoading];
};
/**
* Hook that will fetch data from a url and cache it
* If the isDev flag is set to true, the first fetch will be faked and defaultValue will be returned and set in cache
* @function
* @param {string} url - url to fetch data from
* @param {any} defaultValue - default value to use if the cache is empty
* @param {boolean} useDefault - flag to use the default value
* @param {boolean} clearCache - flag to clear the cache
* @param {boolean} forceError - flag to force an error
* @returns {Array<object|boolean>} data, loading
*/
export const useStaleData = (url, defaultValue = [], useDefault, clearCache, forceError) => {
const [data, setData] = useState(defaultValue);
const [isLoading, setLoading] = useState(true);
if (clearCache) {
delete CACHE[url];
}
// check against forceError flag and throw an error if true
const maybeFakeError = () => {
// Fake an error
if(forceError) {
console.log('force error');
throw new Error('Forced error');
}
};
useEffect(() => {
const cacheId = url;
let mounted = true;
let timeRef = null;
if (useDefault) {
// Emulate a request endpoint
timeRef = setTimeout(() => {
if (!mounted) {
return;
}
maybeFakeError();
setData(defaultValue);
setLoading(false);
}, 100);
// Add a cleanup function for the timeout
return () => {
timeRef && clearTimeout(timeRef);
mounted = false;
};
} else if (CACHE[cacheId] !== undefined) {
setData(CACHE[cacheId]);
setLoading(false);
} else {
const controller = new AbortController();
axios.get(url, { signal: controller.signal }).then(res => {
if (!mounted) {
return;
}
// Fake an error
maybeFakeError();
CACHE[cacheId] = res.data;
setData(res.data);
}).catch(error => {
if (error.name !== 'CanceledError') {
console.log('\t\tError fetching Data', error);
}
}).then(() => {
if (!mounted) {
return;
}
//Set the loaded flag to get rid of the loading message
setLoading(false);
});
// Clenaup function using the abort controller
return () => {
controller.abort();
mounted = false;
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return [data, isLoading];
};
/**
* Hook to use Axios get request without caching
* @function
* @param {string} url - url to fetch
* @param {object} [defaultValue] - default value of the data state
* @param {object} [options] - options for various things
* @param {function} [options.resultMapper] - function to map the results
* @returns {Array<object|boolean|function>} data, isLoading, setData
*/
export const useGet = (url, defaultValue = null, options) => {
const [data, setData] = useState(defaultValue);
const [isLoading, setLoading] = useState(true);
// We monitor the url to see if it has changed
// This important for components that fetch data from the same url but with different parameters
// I.E. ViewCommunity where the ID changes
useEffect(() => {
setLoading(true);
let mounted = true;
const controller = new AbortController();
const getData = () => {
axios.get(url, { signal: controller.signal }).then(res => {
if (mounted) {
const mapper = functionOrDefault(options?.resultMapper);
setData(mapper ? mapper(res.data) : res.data);
}
}
).catch(error => {
if (error.name !== 'CanceledError') {
console.log('Error fetching data', error);
}
}).finally(() => {
if (mounted) {
setLoading(false);
}
});
};
getData();
// Cleanup on unmount
return () => {
controller.abort();
mounted = false;
};
}, [url]);
return [data, isLoading, setData];
};
/**
* Hook to use axios-retry to retry a request if it fails with a 504 error
* This happens when the database is not available
**/
axiosRetry(axios, {
retries: 3, // number of retries
retryDelay: (retryCount) => {
console.log(`retry attempt: ${retryCount}`);
return retryCount * 2000; // time interval between retries
},
retryCondition: (error) => {
// if retry condition is not specified, by default idempotent requests are retried
return error.response.status === 504;
},
});
Source