UNPKG

@selfcommunity/react-core

Version:

React Core Components useful for integrating UI Community components (react-ui).

235 lines (234 loc) • 10.2 kB
import { useEffect, useReducer } from 'react'; import { SCOPE_SC_CORE } from '../constants/Errors'; import { Endpoints, http } from '@selfcommunity/api-services'; import { CacheStrategies, Logger, LRUCache } from '@selfcommunity/utils'; import useSCFetchFeedObject from './useSCFetchFeedObject'; import { getContributorsCacheKey } from '../constants/Cache'; import { useIsComponentMountedRef } from '../utils/hooks'; /** * @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 contributorsObjectActionTypes = { LOADING_NEXT: '_loading_next', LOADING_PREVIOUS: '_loading_previous', DATA_NEXT_LOADED: '_data_next_loaded', DATA_PREVIOUS_LOADED: '_data_previous_loaded', DATA_RELOAD: '_data_reload', DATA_RELOADED: '_data_reloaded', }; /** * contributorsReducer: * - manage the state of contributors object * - update the state base on action type * @param state * @param action */ function contributorsReducer(state, action) { switch (action.type) { case contributorsObjectActionTypes.LOADING_NEXT: return Object.assign(Object.assign({}, state), { isLoadingNext: true, isLoadingPrevious: false }); case contributorsObjectActionTypes.LOADING_PREVIOUS: return Object.assign(Object.assign({}, state), { isLoadingNext: false, isLoadingPrevious: true }); case contributorsObjectActionTypes.DATA_NEXT_LOADED: return Object.assign(Object.assign(Object.assign(Object.assign({}, state), { page: action.payload.currentPage, contributors: action.payload.contributors, isLoadingNext: false, componentLoaded: true, next: action.payload.next }), (action.payload.previous ? { previous: action.payload.previous } : {})), (action.payload.total ? { total: action.payload.total } : {})); case contributorsObjectActionTypes.DATA_PREVIOUS_LOADED: return Object.assign(Object.assign({}, state), { page: action.payload.currentPage, contributors: action.payload.contributors, isLoadingPrevious: false, previous: action.payload.previous }); case contributorsObjectActionTypes.DATA_RELOAD: return Object.assign(Object.assign({}, state), { next: action.payload.next, contributors: [], total: 0, previous: null, reload: true }); case contributorsObjectActionTypes.DATA_RELOADED: return Object.assign(Object.assign({}, state), { componentLoaded: false, reload: false }); default: throw new Error(`Unhandled type: ${action.type}`); } } /** * Define initial state * @param data */ function stateInitializer(data) { const __contributorsObjectCacheKey = data.obj ? getContributorsCacheKey(data.obj.id, data.obj.type, data.next) : null; let _initState = { componentLoaded: false, contributors: [], total: 0, next: data.next, previous: null, isLoadingNext: false, isLoadingPrevious: false, page: data.offset / data.pageSize + 1, reload: false, }; if (__contributorsObjectCacheKey && LRUCache.hasKey(__contributorsObjectCacheKey) && data.cacheStrategy !== CacheStrategies.NETWORK_ONLY) { const _cachedData = LRUCache.get(__contributorsObjectCacheKey); return Object.assign(Object.assign({}, _initState), { total: _cachedData.count, next: _cachedData.next, previous: _cachedData.previous, contributors: _cachedData.results }); } return _initState; } /** :::info This custom hooks is used to fetch paginated contributors. ::: * @param props */ export default function useSCFetchContributors(props) { // PROPS const { id, feedObject, feedObjectType, offset = 0, pageSize = 5, onChangePage, cacheStrategy = CacheStrategies.CACHE_FIRST } = props; // REFS const isMountedRef = useIsComponentMountedRef(); // FeedObject const { obj, setObj } = useSCFetchFeedObject({ id, feedObject, feedObjectType }); const objId = obj ? obj.id : null; /** * Get next url */ const getNextUrl = () => { const _offset = offset ? `&offset=${offset}` : ''; const _objectId = obj ? obj.id : id; const _typeObject = obj ? obj.type : feedObjectType; return `${Endpoints.FeedObjectContributorsList.url({ type: _typeObject, id: _objectId })}?limit=${pageSize}${_offset}`; }; // STATE const [state, dispatch] = useReducer(contributorsReducer, {}, () => stateInitializer({ obj, offset, pageSize, next: getNextUrl(), cacheStrategy })); /** * Calculate current page */ const getCurrentPage = (url) => { const urlSearchParams = new URLSearchParams(url); const params = Object.fromEntries(urlSearchParams.entries()); const currentOffset = params.offset ? parseInt(params.offset) : 0; return currentOffset / pageSize + 1; }; /** * Get Comments (with cache) */ const revalidate = (url, forward) => { return performFetchComments(url, false).then((res) => { let _contributors; let currentPage = getCurrentPage(state.next); if (forward) { let start = state.contributors.slice(0, state.contributors.length - res.results.length); _contributors = start.concat(res.results); } else { let start = state.contributors.slice(res.results.length, state.contributors.length); _contributors = res.results.concat(start); } if (isMountedRef.current) { dispatch({ type: forward ? contributorsObjectActionTypes.DATA_NEXT_LOADED : contributorsObjectActionTypes.DATA_PREVIOUS_LOADED, payload: Object.assign(Object.assign({ page: currentPage, contributors: _contributors }, (forward ? { next: res.next } : { previous: res.previous })), { total: res.count }), }); } }); }; /** * Get Comments */ const performFetchComments = (url, seekCache = true) => { const _contributorsCacheKey = getContributorsCacheKey(obj.id, obj.type, url); if (seekCache && LRUCache.hasKey(_contributorsCacheKey) && cacheStrategy !== CacheStrategies.NETWORK_ONLY) { return Promise.resolve(LRUCache.get(_contributorsCacheKey)); } return http .request({ url, method: Endpoints.Comments.method, }) .then((res) => { if (res.status >= 300) { return Promise.reject(res); } LRUCache.set(_contributorsCacheKey, res.data); return Promise.resolve(res.data); }); }; /** * Fetch previous contributors */ function getPreviousPage() { if (obj && state.previous && !state.isLoadingPrevious) { dispatch({ type: contributorsObjectActionTypes.LOADING_PREVIOUS }); performFetchComments(state.previous) .then((res) => { if (isMountedRef.current) { let currentPage = getCurrentPage(state.previous); dispatch({ type: contributorsObjectActionTypes.DATA_PREVIOUS_LOADED, payload: { page: currentPage, contributors: [...res.results, ...state.contributors], previous: res.previous, }, }); onChangePage && onChangePage(currentPage); } }) .catch((error) => { Logger.error(SCOPE_SC_CORE, error); }) .then(() => { if (cacheStrategy === CacheStrategies.STALE_WHILE_REVALIDATE) { revalidate(state.next, false); } }); } } /** * Fetch next contributors */ function getNextPage() { if (obj && state.next && !state.isLoadingNext) { dispatch({ type: contributorsObjectActionTypes.LOADING_NEXT }); performFetchComments(state.next) .then((res) => { if (isMountedRef.current) { let currentPage = getCurrentPage(state.next); dispatch({ type: contributorsObjectActionTypes.DATA_NEXT_LOADED, payload: Object.assign({ page: currentPage, contributors: [...state.contributors, ...res.results], next: res.next, total: res.count, componentLoaded: true }, (offset && state.contributors.length === 0 ? { previous: res.previous } : {})), }); onChangePage && onChangePage(currentPage); } }) .catch((error) => { Logger.error(SCOPE_SC_CORE, error); }) .then(() => { if (isMountedRef.current && cacheStrategy === CacheStrategies.STALE_WHILE_REVALIDATE) { revalidate(state.next, true); } }); } } /** * Reset contributors status on change pageSize, offset */ useEffect(() => { if (isMountedRef.current && state.componentLoaded && Boolean(obj) && !state.reload) { dispatch({ type: contributorsObjectActionTypes.DATA_RELOAD, payload: { next: getNextUrl(), }, }); } }, [objId, pageSize, offset, isMountedRef]); /** * Reload fetch contributors */ useEffect(() => { if (isMountedRef.current && state.componentLoaded && state.reload && !state.isLoadingNext && !state.isLoadingPrevious) { dispatch({ type: contributorsObjectActionTypes.DATA_RELOADED, }); getNextPage(); } }, [state.reload, isMountedRef]); return Object.assign(Object.assign({ feedObject: obj, setFeedObject: setObj }, state), { pageSize, getNextPage, getPreviousPage }); }