@selfcommunity/react-core
Version:
React Core Components useful for integrating UI Community components (react-ui).
275 lines (274 loc) • 12.4 kB
JavaScript
import { useEffect, useMemo, useReducer } from 'react';
import { SCOPE_SC_CORE } from '../constants/Errors';
import { http } from '@selfcommunity/api-services';
import { CacheStrategies, Logger, LRUCache } from '@selfcommunity/utils';
import { getFeedCacheKey, getStateFeedCacheKey } from '../constants/Cache';
import { appendURLSearchParams } from '@selfcommunity/utils';
import useIsComponentMountedRef from '../utils/hooks/useIsComponentMountedRef';
/**
* @hidden
* We have complex state logic that involves multiple sub-values,
* so useReducer is preferable to useState.
* Define all possible auth action types label
* Use this to export actions and dispatch an action
*/
export const feedDataActionTypes = {
LOADING_NEXT: '_loading_next',
LOADING_PREVIOUS: '_loading_previous',
DATA_NEXT_LOADED: '_data_next_loaded',
DATA_PREVIOUS_LOADED: '_data_previous_loaded',
DATA_REVALIDATE: '_data_revalidate',
DATA_RELOAD: '_data_reload',
DATA_RELOADED: '_data_reloaded',
UPDATE_DATA: '_data_update',
RESET: '_reset',
};
/**
* feedDataReducer:
* - manage the state of feed object
* - update the state base on action type
* @param state
* @param action
*/
function feedDataReducer(state, action) {
let _state = Object.assign({}, state);
switch (action.type) {
case feedDataActionTypes.LOADING_NEXT:
_state = Object.assign(Object.assign({}, state), { isLoadingNext: true, isLoadingPrevious: false });
break;
case feedDataActionTypes.LOADING_PREVIOUS:
_state = Object.assign(Object.assign({}, state), { isLoadingNext: false, isLoadingPrevious: true });
break;
case feedDataActionTypes.DATA_NEXT_LOADED:
_state = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, state), { currentPage: action.payload.currentPage, currentOffset: action.payload.currentOffset, results: [...state.results, ...action.payload.results], isLoadingNext: false, componentLoaded: true, next: action.payload.next, nextPage: action.payload.nextPage }), (action.payload.previous ? { previous: action.payload.previous } : {})), (action.payload.count ? { count: action.payload.count } : {})), (action.payload.previousPage ? { previousPage: action.payload.previousPage } : {}));
break;
case feedDataActionTypes.DATA_PREVIOUS_LOADED:
_state = Object.assign(Object.assign({}, state), { currentPage: action.payload.currentPage, currentOffset: action.payload.currentOffset, initialOffset: action.payload.initialOffset, results: [...action.payload.results, ...state.results], isLoadingPrevious: false, componentLoaded: true, previous: action.payload.previous, previousPage: action.payload.previousPage });
break;
case feedDataActionTypes.DATA_REVALIDATE:
_state = Object.assign(Object.assign({}, state), { results: action.payload.results });
break;
case feedDataActionTypes.DATA_RELOAD:
_state = Object.assign(Object.assign({}, state), { next: action.payload.next, currentPage: 1, previousPage: null, nextPage: null, currentOffset: 0, initialOffset: 0, results: [], count: 0, previous: null, reload: true });
break;
case feedDataActionTypes.DATA_RELOADED:
_state = Object.assign(Object.assign({}, state), { componentLoaded: false, reload: false });
break;
case feedDataActionTypes.UPDATE_DATA:
_state = Object.assign(Object.assign({}, state), action.payload);
break;
case feedDataActionTypes.RESET:
_state = Object.assign({}, action.payload);
break;
}
LRUCache.set(getStateFeedCacheKey(state.id), _state);
return _state;
}
/**
* Define initial state
* @param data
*/
function stateInitializer(data) {
let _initState = Object.assign({ id: data.id, results: [], count: 0, next: data.next, previous: null, isLoadingNext: false, isLoadingPrevious: false, limit: data.queryParams.limit, currentPage: Math.ceil(data.queryParams.offset / data.queryParams.limit + 1), currentOffset: data.queryParams.offset, initialOffset: data.queryParams.offset, reload: false, componentLoaded: Boolean(data.prefetchedData) }, (data.prefetchedData && data.prefetchedData));
_initState['nextPage'] = _initState.next ? _initState.currentPage + 1 : null;
_initState['previousPage'] = _initState.previous ? _initState.currentPage - 1 : null;
const __feedStateCacheKey = getStateFeedCacheKey(data.id);
if (__feedStateCacheKey && LRUCache.hasKey(__feedStateCacheKey) && data.cacheStrategy !== CacheStrategies.NETWORK_ONLY) {
const _cachedStateData = LRUCache.get(__feedStateCacheKey);
return Object.assign(Object.assign({}, _initState), _cachedStateData);
}
else if (data.prefetchedData) {
LRUCache.set(__feedStateCacheKey, _initState);
}
return _initState;
}
/**
:::info
This custom hooks is used to fetch paginated Data.
:::
* @param props
*/
export default function useSCFetchFeed(props) {
// PROPS
const { id, endpoint, endpointQueryParams = { limit: 5, offset: 0 }, onNextPage, onPreviousPage, cacheStrategy = CacheStrategies.NETWORK_ONLY, prefetchedData, } = props;
const queryParams = useMemo(() => Object.assign({ limit: 10, offset: 0 }, endpointQueryParams), [endpointQueryParams]);
/**
* Track component initialization
*/
const isMountedRef = useIsComponentMountedRef();
/**
* Get next url
*/
const getInitialNextUrl = useMemo(() => () => {
const _initialEndpoint = appendURLSearchParams(endpoint.url({}), Object.keys(queryParams).map((k) => ({ [k]: queryParams[k] })));
return _initialEndpoint;
}, [queryParams]);
// STATE
const [state, dispatch] = useReducer(feedDataReducer, {}, () => stateInitializer({ id, endpoint, queryParams, next: getInitialNextUrl(), cacheStrategy, prefetchedData }));
/**
* Calculate current page
*/
const getCurrentOffset = (url) => {
const urlSearchParams = new URLSearchParams(url);
const params = Object.fromEntries(urlSearchParams.entries());
return params.offset ? parseInt(params.offset) : 0;
};
/**
* Feed revalidation
*/
const revalidate = (url, forward) => {
return performFetchData(url, false).then((res) => {
let _data;
if (forward) {
let start = state.results.slice(0, state.results.length - res.results.length);
_data = start.concat(res.results);
}
else {
let start = state.results.slice(res.results.length, state.results.length);
_data = res.results.concat(start);
}
dispatch({
type: feedDataActionTypes.DATA_REVALIDATE,
payload: { results: _data },
});
});
};
/**
* Get Feed data
*/
const performFetchData = (url, seekCache = true, source) => {
const __feedDataCacheKey = getFeedCacheKey(id, url);
if (seekCache && LRUCache.hasKey(__feedDataCacheKey) && cacheStrategy !== CacheStrategies.NETWORK_ONLY) {
return Promise.resolve(LRUCache.get(__feedDataCacheKey));
}
return http
.request(Object.assign({ url, method: endpoint.method }, (source ? { cancelToken: source.token } : {})))
.then((res) => {
if (res.status >= 300) {
return Promise.reject(res);
}
LRUCache.set(__feedDataCacheKey, res.data);
return Promise.resolve(res.data);
});
};
/**
* Fetch previous data
*/
function getPreviousPage() {
if (endpoint && state.previous && !state.isLoadingPrevious) {
dispatch({ type: feedDataActionTypes.LOADING_PREVIOUS });
performFetchData(state.previous)
.then((res) => {
if (isMountedRef.current) {
let currentOffset = Math.max(getCurrentOffset(state.previous), 0);
let currentPage = Math.ceil(currentOffset / queryParams.limit + 1);
let previousPage = res.previous ? currentPage - 1 : null;
let count = res.count || state.count + res.results.length + 1;
dispatch({
type: feedDataActionTypes.DATA_PREVIOUS_LOADED,
payload: {
currentPage,
previousPage,
currentOffset,
count,
initialOffset: currentOffset,
results: res.results,
previous: res.previous,
},
});
onPreviousPage && onPreviousPage(currentPage, currentOffset, count, res.results);
if (cacheStrategy === CacheStrategies.STALE_WHILE_REVALIDATE) {
revalidate(state.next, true);
}
}
})
.catch((error) => {
Logger.error(SCOPE_SC_CORE, error);
});
}
}
/**
* Fetch next data
*/
function getNextPage(source = false) {
if (endpoint && state.next && !state.isLoadingNext) {
dispatch({ type: feedDataActionTypes.LOADING_NEXT });
performFetchData(state.next, null, source)
.then((res) => {
if (isMountedRef.current) {
let currentOffset = Math.max(getCurrentOffset(res.next) - queryParams.limit, state.results.length);
let currentPage = Math.ceil(currentOffset / queryParams.limit + 1);
let nextPage = res.next ? currentPage + 1 : null;
let count = res.count || (state.count === 0 && res.results.length === 0 ? 0 : state.count + res.results.length + 1);
dispatch({
type: feedDataActionTypes.DATA_NEXT_LOADED,
payload: Object.assign({ currentPage,
nextPage,
currentOffset,
count, results: res.results, next: res.next, componentLoaded: true }, (queryParams.offset && state.results.length === 0
? { previous: res.previous, previousPage: res.previous ? currentPage - 1 : null }
: {})),
});
onNextPage && onNextPage(currentPage, currentOffset, count, res.results);
if (cacheStrategy === CacheStrategies.STALE_WHILE_REVALIDATE) {
revalidate(state.next, true);
}
}
})
.catch((error) => {
Logger.error(SCOPE_SC_CORE, error);
});
}
}
/**
* Reload
*/
function reload() {
dispatch({
type: feedDataActionTypes.DATA_RELOAD,
payload: {
next: getInitialNextUrl(),
},
});
}
/**
* Update component state
* Re-sync next/previous url
* When an element is added in the head of a rendered list, fix the next/previous url
* to avoid importing posts already in the list
* @param payload
*/
function updateState(payload) {
dispatch({ type: feedDataActionTypes.UPDATE_DATA, payload: payload });
}
/**
* Reset state component
*/
function reset() {
dispatch({
type: feedDataActionTypes.RESET,
payload: stateInitializer({ id, endpoint, queryParams, next: getInitialNextUrl(), cacheStrategy, prefetchedData }),
});
}
/**
* Reload fetch data
*/
useEffect(() => {
if (state.componentLoaded && state.reload && !state.isLoadingNext && !state.isLoadingPrevious) {
dispatch({
type: feedDataActionTypes.DATA_RELOADED,
});
getNextPage();
}
}, [state.reload]);
useEffect(() => {
return () => {
reset();
};
}, []);
return Object.assign(Object.assign({}, state), { updateState,
getNextPage,
getPreviousPage,
reload,
reset });
}