Last active
October 18, 2022 22:20
-
-
Save jenya239/f5d4fa49118c7144718f4a809ee242fe to your computer and use it in GitHub Desktop.
fetchable by page
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { | |
createContext, | |
Reducer, | |
RefObject, | |
useCallback, | |
useContext, | |
useMemo, | |
useReducer, | |
useRef, | |
} from 'react' | |
import { deepFreeze, IWithChildren } from 'utils' | |
let idCounter = 0 | |
type Status = 'notLoaded' | 'loading' | 'error' | 'success' | |
const EMPTY_ARRAY = Object.freeze([]) | |
export type PageableState<Item, Params, Data> = { | |
id: number | |
status: Status | |
params: Params | |
pages: readonly Item[][] | |
ids: number[] | |
error: string | |
data: Data | |
totalCount: number | |
totalPages: number | |
} | |
type Action<Item, Params> = | |
| { | |
type: 'request' | |
id: number | |
params: Params | |
} | |
| { | |
type: 'success' | |
id: number | |
found: Item[] | |
totalCount: number | |
} | |
| { type: 'failure'; id: number; error: string } | |
| { type: 'reset'; initial?: Item[]; params?: Params; totalCount?: number } | |
const getDefaultState = <Item, Params, Data>( | |
getDefaultParams: () => Params, | |
getDefaultData: () => Data | |
): PageableState<Item, Params, Data> => | |
deepFreeze({ | |
id: -1, | |
status: 'notLoaded', | |
params: getDefaultParams(), | |
pages: EMPTY_ARRAY, | |
ids: [], | |
error: '', | |
data: getDefaultData(), | |
totalCount: -1, | |
totalPages: -1, | |
}) | |
/** | |
* если уже идёт загрузка | |
* если параметры те же | |
* ничего не делать | |
* если отличаются | |
* прервать | |
* начать новую | |
* если нет загрузки | |
* если параметры те же | |
* грузить след. страницу | |
* если отличаются | |
* грузить первую | |
*/ | |
// posible better without reducer. just changable store | |
const getReducer = | |
<Item extends { id: number }, Params, Data>( | |
paramsCompare: (params1: Params, params2: Params) => boolean, | |
itemsPerPage: number, | |
getData: (items: Item[]) => Data, | |
getDefaultParams: () => Params, | |
getDefaultData: () => Data | |
) => | |
(state: PageableState<Item, Params, Data>, action: Action<Item, Params>) => { | |
const { status, params, id, ids } = state | |
const res: PageableState<Item, Params, Data> = { ...state } | |
let sameParams: boolean | |
let found: Item[] | |
let defaultState | |
switch (action.type) { | |
case 'request': | |
res.id = action.id | |
res.params = action.params | |
// res.pages = action.nextPage ? res.pages : EMPTY_ARRAY | |
res.status = 'loading' | |
sameParams = paramsCompare(params, action.params) | |
if (status === 'loading') { | |
if (sameParams) { | |
console.log('already same params') | |
// console.log('state', state) | |
return state | |
} else { | |
res.pages = EMPTY_ARRAY | |
res.ids = [] | |
} | |
} else { | |
if (!sameParams) { | |
res.pages = EMPTY_ARRAY | |
res.ids = [] | |
} | |
} | |
break | |
case 'success': | |
if (state.status !== 'loading') { | |
console.log('=====incorrect status', state.status) | |
break // ? | |
} | |
if (id !== action.id) { | |
console.log('id mismatch', id, action.id) | |
break | |
} | |
// если нашёлся двойник, то всё сбрасываем и запрашиваем заново | |
found = action.found.filter((item) => !ids.includes(item.id)) | |
res.pages = res.pages.concat([found]) | |
res.ids = res.ids.concat(found.map((item) => item.id)) | |
res.totalCount = action.totalCount | |
res.totalPages = Math.ceil(action.totalCount / itemsPerPage) | |
res.status = 'success' | |
res.data = getData(res.pages.flat()) | |
break | |
case 'failure': | |
if (id !== action.id) { | |
console.log('id mismatch', id, action.id) | |
break | |
} | |
res.error = action.error | |
res.status = 'error' | |
break | |
case 'reset': | |
defaultState = getDefaultState(getDefaultParams, getDefaultData) | |
return action.initial | |
? { | |
...defaultState, | |
id: idCounter++, | |
pages: [action.initial], | |
status: 'success', | |
ids: action.initial.map((item) => item.id), | |
data: getData(action.initial), | |
totalCount: action.totalCount || defaultState.totalCount, | |
params: action.params || defaultState.params, | |
totalPages: action.totalCount | |
? Math.ceil(action.totalCount / itemsPerPage) | |
: defaultState.totalCount, | |
} | |
: defaultState | |
} | |
// console.log('state', res) | |
return deepFreeze(res) | |
} | |
interface IActions<Params> { | |
processFetch: (params: Params) => Promise<void> | |
reset: () => void | |
} | |
export type IPageableContext<Item extends { id: number }, Params, Data> = PageableState< | |
Item, | |
Params, | |
Data | |
> & | |
IActions<Params> & { stateRef: RefObject<PageableState<Item, Params, Data>> } | |
interface IProviderProps<Item, Params> extends IWithChildren { | |
fetchItems: (params: Params, pageIndex: number, itemsPerPage: number) => Promise<[Item[], number]> | |
} | |
export interface IPageableStore<Item extends { id: number }, Params, Data> { | |
provider: (props: IProviderProps<Item, Params>) => JSX.Element | |
consumer: () => IPageableContext<Item, Params, Data> | |
} | |
export const createPageableStore = <Item extends { id: number }, Params, Data>( | |
paramsCompare: (params1: Params, params2: Params) => boolean, | |
itemsPerPage: number, | |
getData: (items: Item[]) => Data, | |
getDefaultParams: () => Params, | |
getDefaultData: () => Data | |
): IPageableStore<Item, Params, Data> => { | |
const Context = createContext<IPageableContext<Item, Params, Data> | undefined>(undefined) | |
const Provider = ({ children, fetchItems }: IProviderProps<Item, Params>): JSX.Element => { | |
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | |
// @ts-ignore | |
const [state, dispatch] = useReducer< | |
Reducer<PageableState<Item, Params, Data>, Action<Item, Params>> | |
>( | |
getReducer( | |
paramsCompare, | |
itemsPerPage, | |
getData, | |
getDefaultParams, | |
getDefaultData | |
) as unknown as Reducer<PageableState<Item, Params, Data>, Action<Item, Params>>, | |
getDefaultState(getDefaultParams, getDefaultData) | |
) | |
const stateRef = useRef(state) | |
stateRef.current = state | |
// how pageIndex check | |
/** | |
* если уже идёт загрузка | |
* если параметры те же | |
* ничего не делать | |
* если отличаются | |
* прервать | |
* начать новую | |
* если нет загрузки | |
* если параметры те же | |
* грузить след. страницу | |
* если отличаются | |
* грузить первую | |
*/ | |
const processFecth = useCallback(async (params: Params): Promise<void> => { | |
if (!stateRef.current) return | |
const sameParams = paramsCompare(stateRef.current.params, params) | |
if (stateRef.current.status === 'loading' && sameParams) { | |
console.log('already process same params') | |
return | |
} | |
const id = idCounter++ | |
const pageIndex = | |
stateRef.current.status !== 'loading' && sameParams ? stateRef.current.pages.length : 0 | |
dispatch({ | |
id, | |
type: 'request', | |
params, | |
}) | |
try { | |
const [found, totalCount] = await fetchItems(params, pageIndex, itemsPerPage) | |
// если нашёлся двойник, то всё сбрасываем и запрашиваем заново | |
dispatch({ | |
type: 'success', | |
id, | |
found, | |
totalCount, | |
}) | |
} catch (e) { | |
console.error('pageable error', e) | |
dispatch({ | |
type: 'failure', | |
id, | |
error: '' + e, | |
}) | |
} | |
}, []) | |
const actions = useMemo<IActions<Params>>( | |
() => ({ | |
processFetch: processFecth, | |
reset: () => dispatch({ type: 'reset' }), | |
}), | |
[processFecth] | |
) | |
return ( | |
<Context.Provider value={actions ? { ...state, ...actions, stateRef } : undefined}> | |
{children} | |
</Context.Provider> | |
) | |
} | |
return Object.freeze({ | |
provider: Provider, | |
consumer: () => { | |
const context = useContext(Context) | |
if (context === undefined) | |
throw new Error(`useContext must be used within a PageableProvider`) | |
return context | |
}, | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment