Created
August 18, 2020 01:05
-
-
Save emeraldsanto/6f34d7e05f90f31dc25e9bc4807bcc6b to your computer and use it in GitHub Desktop.
A wrapper over the base FlatList component with better pagination and automatic data-retrieval footers.
This file contains 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 { RefObject } from "react"; | |
import { FlatList, FlatListProps } from "react-native"; | |
import Success from "@assets/icons/success.svg"; | |
import Warning from "@assets/icons/warning.svg"; | |
import { ColorCode } from "@models/constants/ColorCode"; | |
import { Row } from "@primitives/Flex/Row"; | |
import { LocalizedText } from "@primitives/LocalizedText/LocalizedText"; | |
import { useScrollToTop } from "@react-navigation/native"; | |
import React, { FC, Fragment, useImperativeHandle, useRef, useState } from "react"; | |
import { ActivityIndicator, FlatList, StyleSheet } from "react-native"; | |
import { PaginatedListStyle } from "./PaginatedListStyle"; | |
type BaseFlatListProps<T> = Omit<FlatListProps<T>, "onEndReached">; | |
export type DataRetrievalStatus = "available" | "unavailable" | "failed"; | |
/** | |
* Function to be called once loading of paginated data was completed. | |
* @param {boolean} success A boolean indicating wether or not the fetching succeeded, | |
* will only increment page if `success` is true | |
*/ | |
export type PaginationCallback = (status: DataRetrievalStatus) => void; | |
export interface PaginatedListProps<T> extends BaseFlatListProps<T> { | |
statusFooterDisabled?: boolean; | |
innerRef?: RefObject<PaginatedListRef>; | |
flatListRef?: (instance: FlatList<T> | null) => void; | |
onPageEndReached: (nextPage: number, done: PaginationCallback) => void; | |
} | |
export interface PaginatedListRef { | |
resetPageCounter: (count?: number) => void; | |
resetWorkingStatus: (busy?: boolean) => void; | |
resetDataRetrievalStatus: (status?: DataRetrievalStatus) => void; | |
} | |
export function PaginatedList<T>(props: PaginatedListProps<T>) { | |
const { statusFooterDisabled, innerRef, onPageEndReached, ListFooterComponent, flatListRef, ...rest } = props; | |
const [page, setPage] = useState<number>(0); | |
const [working, setWorking] = useState<boolean>(false); | |
const [currentStatus, setCurrentStatus] = useState<DataRetrievalStatus>("available"); | |
const listRef = useRef<FlatList<T> | null>(null); | |
useScrollToTop(listRef); | |
useImperativeHandle( | |
innerRef, | |
() => ({ | |
resetPageCounter, | |
resetWorkingStatus, | |
resetDataRetrievalStatus | |
}) | |
); | |
function resetPageCounter(count: number = 0) { | |
resetDataRetrievalStatus("available"); | |
resetWorkingStatus(false); | |
setPage(count); | |
} | |
function resetWorkingStatus(busy: boolean = false) { | |
setWorking(busy); | |
} | |
function resetDataRetrievalStatus(status: DataRetrievalStatus = "available") { | |
setCurrentStatus(status); | |
} | |
function handleRef(instance: FlatList<T> | null) { | |
listRef.current = instance; | |
flatListRef?.(instance); | |
} | |
function onEndReached(info: { distanceFromEnd: number }) { | |
if (working || currentStatus !== "available" || info.distanceFromEnd < 0) { | |
return; | |
} | |
setWorking(true); | |
onPageEndReached(page + 1, (status: DataRetrievalStatus) => { | |
setWorking(false); | |
setCurrentStatus(status); | |
if (status === "available") { | |
setPage(prev => prev + 1); | |
} | |
}); | |
} | |
function renderFooter() { | |
if (statusFooterDisabled || props.data?.length === 0) { | |
return null; | |
} | |
else if (working) { | |
return ( | |
<ActivityIndicator | |
size="large" | |
color={ColorCode.PRIMARY} | |
style={PaginatedListStyle.indicator} | |
/> | |
); | |
} | |
else if (currentStatus === "unavailable") { | |
return ( | |
<CaughtUp /> | |
); | |
} | |
else if (currentStatus === "failed") { | |
return ( | |
<NetworkError /> | |
); | |
} | |
return null; | |
} | |
return ( | |
<FlatList | |
{...rest} | |
ref={handleRef} | |
removeClippedSubviews | |
onEndReached={onEndReached} | |
ListFooterComponent={ | |
<Fragment> | |
{ListFooterComponent} | |
{renderFooter()} | |
</Fragment> | |
} | |
/> | |
); | |
} | |
const PaginatedListStyle = StyleSheet.create({ | |
indicator: { | |
marginTop: Spacing.SMALL, | |
marginBottom: 25 | |
}, | |
footer: { | |
margin: Spacing.DEFAULT | |
}, | |
icon: { | |
width: 28, | |
height: 28, | |
marginEnd: Spacing.SMALL | |
}, | |
text: { | |
flex: 1, | |
fontFamily: FontFamily.ROBOTO.MEDIUM | |
} | |
}); | |
const CaughtUp: FC = () => { | |
return ( | |
<Row alignItems="center" justifyContent="center" style={PaginatedListStyle.footer}> | |
<Success {...PaginatedListStyle.icon} /> | |
<LocalizedText style={PaginatedListStyle.text}> | |
allCaughtUp | |
</LocalizedText> | |
</Row> | |
) | |
} | |
const NetworkError: FC = () => { | |
return ( | |
<Row alignItems="center" justifyContent="center" style={PaginatedListStyle.footer}> | |
<Warning {...PaginatedListStyle.icon} /> | |
<LocalizedText style={PaginatedListStyle.text}> | |
errorLoadingContent | |
</LocalizedText> | |
</Row> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment