Skip to content

Instantly share code, notes, and snippets.

@juangl
Created May 7, 2021 20:01
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 juangl/dd174420a665766cd3478029fbf132dc to your computer and use it in GitHub Desktop.
Save juangl/dd174420a665766cd3478029fbf132dc to your computer and use it in GitHub Desktop.
import React, { useCallback } from 'react';
type FetchReducerActions<DataT> =
| { type: 'REQUEST_START' }
| { type: 'REQUEST_SUCCESS'; payload: DataT }
| { type: 'REQUEST_ERROR'; error: Error }
| { type: 'FETCH_NEXT_PAGE' }
// use to fetch again after an error
| { type: 'RETRY' };
type RequestState = 'idle' | 'loading' | 'success' | 'failure';
type FetchState<DataT> = {
data: DataT[];
lastLoadedPage: number;
lastRequestedPage: number;
requestState: RequestState;
error: Error | null;
};
function fetchReducer<DataT>(state: FetchState<DataT>, action: FetchReducerActions<DataT>) {
if (action.type === 'FETCH_NEXT_PAGE') {
return {
...state,
lastRequestedPage: state.lastLoadedPage + 1,
};
}
if (action.type === 'REQUEST_START') {
return {
...state,
requestState: 'loading' as const,
error: null,
};
}
if (action.type === 'REQUEST_SUCCESS') {
const pagesTmp = [...state.data];
pagesTmp[state.lastRequestedPage] = action.payload;
return {
...state,
requestState: 'success' as const,
lastLoadedPage: state.lastLoadedPage + 1,
data: pagesTmp,
};
}
if (action.type === 'REQUEST_ERROR') {
return {
...state,
requestState: 'failure' as const,
error: action.error,
};
}
if (action.type === 'RETRY') {
return {
...state,
requestState: 'idle' as const,
error: null,
};
}
return state;
}
type FetcherArgs<DataT> = { index: number; previousPageData: DataT };
type Fetcher<DataT> = (args: FetcherArgs<DataT>) => Promise<DataT>;
type UseInfiniteFetchOptions<DataT> = {
enabled?: boolean;
};
/**
* API based in React Query: https://react-query.tanstack.com/guides/infinite-queries
*/
// eslint-disable-next-line import/prefer-default-export
export function useInfiniteFetch<DataT = any[]>(fetcher: Fetcher<DataT>, options: UseInfiniteFetchOptions<DataT> = {}) {
const { enabled = true } = options;
// this type couldn't be inferred automatically
type ReducerType = React.Reducer<FetchState<DataT>, FetchReducerActions<DataT>>;
const [state, dispatch] = React.useReducer<ReducerType>(fetchReducer, {
data: [],
lastLoadedPage: -1,
lastRequestedPage: 0,
requestState: 'idle',
error: null,
});
const fetcherRef = React.useRef<typeof fetcher>();
React.useEffect(() => {
fetcherRef.current = fetcher;
});
React.useEffect(() => {
if (enabled && state.requestState === 'idle' && state.lastLoadedPage < state.lastRequestedPage) {
dispatch({ type: 'REQUEST_START' });
fetcherRef
.current({ previousPageData: state.data[state.lastLoadedPage], index: state.lastRequestedPage })
.then((payload) => {
dispatch({ type: 'REQUEST_SUCCESS', payload });
})
.catch((error) => {
dispatch({ type: 'REQUEST_ERROR', error });
});
}
}, [enabled, dispatch, state.data, state.lastLoadedPage, state.lastRequestedPage, state.requestState]);
const fetchNextPage = useCallback(() => {
dispatch({ type: 'FETCH_NEXT_PAGE' });
}, [dispatch]);
return {
data: state.data,
error: state.error,
requestState: state.requestState,
fetchNextPage,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment