Last active
July 29, 2021 11:25
-
-
Save fabn/cac865a4da6e903d796eb7a13764855f to your computer and use it in GitHub Desktop.
Pagination with Reactfire and firebase
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 { PAGE_SIZES, usePaginatedCollection } from "./paginationState"; | |
import firebase from "firebase/app"; | |
export type FirestoreUser = { | |
email: string; | |
address: string; | |
zip: string; | |
city: string; | |
name: string; | |
} | |
/** | |
* Return list of all users ordered by email | |
*/ | |
export function AllUsersList() { | |
const query = useFirestore() | |
.collection("users") | |
.orderBy("email") as firebase.firestore.Query<FirestoreUser>; | |
return <FirebaseUsersList query={query} />; | |
} | |
type FirebaseUsersListProps = { | |
query: firebase.firestore.Query<FirestoreUser>; | |
}; | |
export default function FirebaseUsersList({ query }: FirebaseUsersListProps) { | |
const { | |
status, | |
error, | |
pageNumber, | |
rowsPerPage, | |
hasMore, | |
currentPage, | |
changePageSize, | |
nextPage, | |
previousPage, | |
} = usePaginatedCollection<FirestoreUser>({ baseQuery: query }); | |
if (error) { | |
console.error(error); | |
return <>{error.message}</>; | |
} | |
if (status == "loading") | |
return ( | |
<Backdrop open> | |
<CircularProgress size="10rem" /> | |
</Backdrop> | |
); | |
return ( | |
<UsersList | |
users={currentPage} | |
page={pageNumber} | |
rowsPerPage={rowsPerPage} | |
hasMore={hasMore} | |
changePageSize={changePageSize} | |
nextPage={nextPage} | |
previousPage={previousPage} | |
/> | |
); | |
} |
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 firebase from "firebase/app"; | |
import { useEffect, useReducer } from "react"; | |
import { ObservableStatus, useFirestoreCollection } from "reactfire"; | |
export const PAGE_SIZES = [10, 20, 50]; | |
export type PaginationState<T = firebase.firestore.DocumentData> = { | |
page: number; | |
rowsPerPage: number; | |
firstRecord?: firebase.firestore.QueryDocumentSnapshot<T>; | |
lastRecord?: firebase.firestore.QueryDocumentSnapshot<T>; | |
hasMore: boolean; | |
currentPage: firebase.firestore.QueryDocumentSnapshot<T>[]; | |
}; | |
export enum ActionKind { | |
NextPage = "NEXT_PAGE", | |
PreviousPage = "PREVIOUS_PAGE", | |
ChangePageSize = "CHANGE_PAGE_SIZE", | |
RecordsLoaded = "PAGE_LOADED", | |
} | |
export type Action<T = firebase.firestore.DocumentData> = | |
| { | |
type: ActionKind.NextPage; | |
page: firebase.firestore.QueryDocumentSnapshot<T>[]; | |
} | |
| { | |
type: ActionKind.PreviousPage; | |
page: firebase.firestore.QueryDocumentSnapshot<T>[]; | |
} | |
| { type: ActionKind.ChangePageSize; pageSize: number } | |
| { | |
type: ActionKind.RecordsLoaded; | |
data: firebase.firestore.QuerySnapshot<T>; | |
}; | |
export const nextPage: <T = firebase.firestore.DocumentData>( | |
page: firebase.firestore.QueryDocumentSnapshot<T>[] | |
) => Action<T> = (page) => ({ | |
type: ActionKind.NextPage, | |
page, | |
}); | |
export const previousPage: <T = firebase.firestore.DocumentData>( | |
page: firebase.firestore.QueryDocumentSnapshot<T>[] | |
) => Action<T> = (page) => ({ | |
type: ActionKind.PreviousPage, | |
page, | |
}); | |
export const changePageSize: <T = firebase.firestore.DocumentData>( | |
n: number | |
) => Action<T> = (pageSize: number) => ({ | |
type: ActionKind.ChangePageSize, | |
pageSize: pageSize, | |
}); | |
export function paginationStateReducer<T = firebase.firestore.DocumentData>( | |
state: PaginationState<T>, | |
action: Action<T> | |
): PaginationState<T> { | |
let page: firebase.firestore.QueryDocumentSnapshot<T>[]; | |
switch (action.type) { | |
case ActionKind.NextPage: | |
page = action.page; | |
return { | |
...state, | |
page: state.page + 1, | |
lastRecord: page[page.length - 1], | |
firstRecord: undefined, | |
}; | |
case ActionKind.PreviousPage: | |
page = action.page; | |
return { | |
...state, | |
page: state.page - 1, | |
lastRecord: undefined, | |
firstRecord: page[0], | |
}; | |
case ActionKind.ChangePageSize: | |
// reset state and set new page size | |
return { | |
page: 0, | |
firstRecord: undefined, | |
lastRecord: undefined, | |
hasMore: true, | |
currentPage: [], | |
rowsPerPage: action.pageSize, // action payload | |
}; | |
case ActionKind.RecordsLoaded: | |
return { | |
...state, | |
currentPage: action.data?.docs, | |
hasMore: action.data?.docs.length >= state.rowsPerPage, | |
}; | |
default: | |
throw new Error(`Action not implemented ${action}`); | |
} | |
} | |
type PaginationHooksProps<T> = { | |
baseQuery: firebase.firestore.Query<T>; | |
pageSize?: number; | |
}; | |
type PaginatedCollectionData<T> = Pick< | |
ObservableStatus<firebase.firestore.QuerySnapshot<T>>, | |
"status" | "error" | |
> & { | |
pageNumber: number; | |
rowsPerPage: number; | |
hasMore: boolean; | |
currentPage: firebase.firestore.QueryDocumentSnapshot<T>[]; | |
changePageSize: (size: number) => void; | |
nextPage: () => void; | |
previousPage: () => void; | |
}; | |
export function usePaginatedCollection<T>({ | |
pageSize = PAGE_SIZES[0], | |
baseQuery, | |
}: PaginationHooksProps<T>): PaginatedCollectionData<T> { | |
// Initial reducer state | |
const initialState: PaginationState<T> = { | |
page: 0, | |
firstRecord: undefined, | |
lastRecord: undefined, | |
hasMore: true, | |
currentPage: [], | |
rowsPerPage: pageSize, | |
}; | |
const [ | |
{ page, rowsPerPage, firstRecord, lastRecord, hasMore, currentPage }, | |
dispatch, | |
] = useReducer<React.Reducer<PaginationState<T>, Action<T>>>( | |
paginationStateReducer, | |
initialState | |
); | |
// Reducer will guarantee that either firstRecord or lastRecord are present, never both | |
// Apply offsets if limits are passed | |
if (lastRecord) | |
baseQuery = baseQuery.limit(rowsPerPage).startAfter(lastRecord); | |
// Apply end offset when going back | |
if (firstRecord) | |
baseQuery = baseQuery.endBefore(firstRecord).limitToLast(rowsPerPage); | |
// Retrieve data | |
const { status, data, error } = useFirestoreCollection<T>(baseQuery); | |
// Disable pagination when no more records are present | |
useEffect(() => { | |
dispatch({ type: ActionKind.RecordsLoaded, data: data }); | |
}, [data]); | |
return { | |
status, | |
error, | |
currentPage, | |
pageNumber: page, | |
rowsPerPage, | |
hasMore, | |
changePageSize: (n: number) => dispatch(changePageSize(n)), | |
nextPage: () => dispatch(nextPage(currentPage)), | |
previousPage: () => dispatch(previousPage(currentPage)), | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment