Skip to content

Instantly share code, notes, and snippets.

@jenya239
Last active October 18, 2022 22:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jenya239/f5d4fa49118c7144718f4a809ee242fe to your computer and use it in GitHub Desktop.
Save jenya239/f5d4fa49118c7144718f4a809ee242fe to your computer and use it in GitHub Desktop.
fetchable by page
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