Skip to content

Instantly share code, notes, and snippets.

@fabn
Last active July 29, 2021 11:25
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 fabn/cac865a4da6e903d796eb7a13764855f to your computer and use it in GitHub Desktop.
Save fabn/cac865a4da6e903d796eb7a13764855f to your computer and use it in GitHub Desktop.
Pagination with Reactfire and firebase
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}
/>
);
}
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