Open iamthefox opened 5 years ago
At the moment there isn't, but this does seem like a very good use case.
Do you have any thoughts on what this might look like from an API point of view?
I will have a think, and put together a plan, but welcome your suggestions...
I will look into it and attempt to come up with something.
Did a bit of a research and firebase queries are very limiting in what can be done.
https://github.com/deltaepsilon/firebase-paginator/blob/master/firebase-paginator.js
^ Above would probably be the most comprehensive library that somewhat solved this issue.
I used hooks to filter data from firestore, we can extend that to implement pagination. Should I raise a PR for that?
@kriss1897 Yes, please do - I'd be interested to see how you've approached it...
@chrisbianca. Okay. Give me a few days.. I'll work on it this weekend.
Anyone ever get a pagination example working?
@kriss1897
i wrote a custom hook to do "Load More" type of pagination. i'm sure it can use some tweaking from more clever folks here, but it works fine for my purposes for now.
import { useState, useEffect, useCallback } from 'react'
import { useCollectionOnce } from 'react-firebase-hooks/firestore'
const useFirestoreLoadMore = queryFn => {
const [query, setQuery] = useState(null)
const [last, setLast] = useState(null)
const [data, setData] = useState([])
const [qData, loading, error] = useCollectionOnce(query)
useEffect(() => {
setData([])
setQuery(queryFn())
}, [queryFn])
useEffect(() => {
if (qData && qData.query.isEqual(query)) {
setLast(qData.docs[qData.docs.length - 1])
setData([...data, ...qData.docs])
}
}, [qData])
const more = useCallback(() => {
setQuery(queryFn().startAfter(last))
}, [queryFn, setQuery, last])
return [[data, loading, error], more]
}
export default useFirestoreLoadMore
you would use it like this:
const MyComponent = () => {
const queryFn = React.useCallback(() => {
let q = firebase
.firestore()
.collection('things')
.orderBy('date', 'desc')
.limit(15)
if (maybeYouWantToChangeSomething) {
q = q.where('something', 'array-contains', maybeYouWantToChangeSomething)
}
return q
}, [maybeYouWantToChangeSomething])
const [[things, loading, error], more] = useFirestoreLoadMore(queryFn)
return (
<div>
{things.map(thing => <Thing key={thing.id} data={thing} />)}
<button onClick={() => more()}>Load More</button>
</div>
)
}
i hope this is useful to someone.
now for Previous/Next pagination i've run into some roadblocks:
first
and last
snapshot, hoping to use query.startAfter(last)
for "Next" and query.endBefore(first)
for "Previous" but endBefore
doesn't work the way you think combined with limit()
. seems you have to track all first
snapshots for every page and use something like query.startAfter(firstOfPreviousPage)
for "Previous"page
state (i.e. disable "Previous" if page === 0
).would love to know how you guys are approaching it.
Another attempt https://github.com/bmcmahen/firestore-pagination-hook
Can confirm that @RobertSasak's package works well. Thanks!
I created an example of hook with react.js for pagination https://gist.github.com/robsonkades/0f256ab05944699b831031c7e6a8aa84
I found this one https://github.com/kodai3/react-firebase-pagination-hooks
any update from react-hooks
contribute?
any update from
react-hooks
contribute?
i'm looking for that too
I actually whipped this up before having seen this Github page, and I came here to share my snippet, but those other hooks look good too.
Here's what I came up with:
import { FirebaseFirestoreTypes } from "@react-native-firebase/firestore";
import { useCallback, useEffect, useMemo, useState } from "react";
const PAGE_SIZE = 20;
export const useCollectionDataStatic = (query: FirebaseFirestoreTypes.Query) => {
const [data, setData] = useState([]);
const [fetching, setFetching] = useState(false);
const dataVals = useMemo(() => {
return data.map(doc => doc.data());
}, [data])
const loadMore = useCallback(() => {
if (fetching) {
return;
}
if (data.length === 0) {
setFetching(true);
query.limit(PAGE_SIZE).get().then(result => {
setData([...result.docs])
}).catch(err => console.log(err)).finally(() => setFetching(false));
} else {
setFetching(true);
query.startAfter(data[data.length - 1]).limit(PAGE_SIZE).get().then(result => {
setData([...data, ...result.docs]);
}).catch(err => console.log(err)).finally(() => setFetching(false));
}
}, [data, dataVals, fetching]);
useEffect(() => {
loadMore();
}, []);
return [data, dataVals, loadMore];
};
Then it was pretty easy to use it:
const [invoices, invoiceDatas, loadMoreInvoices] = useCollectionDataStatic(query);
return (<FlatInvoiceList invoiceList={invoiceDatas} onEndReached={loadMoreInvoices}/>);
Any updates on this feature?
I will start to contribute here with the pagination @chrisbianca. Anyone here knows if already have some progress about that?
I have a lot of experience with firebase realtime database, with complex queries, compound indexes, "rules and typescript synchronization" and pagination. I think that pagination will have similar Params Interface between realtime database and firestore. So, I can start by doing pagination.
@chrisbianca Do you want to help me with prioritization here (rtdb or firestore or to launch both together)?
PS: I want to share my personal progress through this library to reward the open-source community, especially that. I've created an Issue with my idea explained #219
Hello !!!!!!! :). i made my own hook
Componente
const queryFn = useCallback(() => { const q = query( collection(fireStore, "admin_news"), limit(4), orderBy("categoryValue") ); return q; }, []);
const { more, isLoading, data } = usePaginateQuery(queryFn);
Create hook
import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { DocumentData, Query } from "firebase/firestore"; import { startAfter, query, getDocs } from "firebase/firestore";
const usePaginateQuery = (queryFn: () => Query
const resetStates = () => { setData(null); setData([]); setisLoading(false); };
useEffect(() => { if (isMountedRef.current === true) return;
async function fetchQuery() {
try {
isMountedRef.current = true;
setisLoading(true);
const q = query(queryFn());
const querySnapshot = await getDocs(q);
setData([...querySnapshot.docs]);
lastItemRef.current = querySnapshot.docs[querySnapshot.docs.length - 1];
setisLoading(false);
} catch (error) {
resetStates();
setErrorMsg(error.code);
}
}
fetchQuery();
}, [queryFn]);
const more = useCallback(async () => { try { setisLoading(true); const next = query(queryFn(), startAfter(lastItemRef.current)); const querySnapshot = await getDocs(next); setData([...data, ...querySnapshot.docs]); lastItemRef.current = querySnapshot.docs[querySnapshot.docs.length - 1]; setisLoading(false); } catch (error) { resetStates(); setErrorMsg(error.code); } }, [data, queryFn]);
return useMemo( () => ({ more, isLoading, data, errorMsg, }), [more, isLoading, data, errorMsg] ); };
export default usePaginateQuery;
Any updates on this? The given examples are great, but lack onSnapshot behaviour. I'm trying to create a chat with that, so definitely need pagination, but also the onSnapshot behavior to live stream new messages, any ideas?
import { useState, useRef, useEffect, useCallback } from "react";
import { collection, query, orderBy, startAfter, limit, getCountFromServer, DocumentSnapshot, QueryConstraint } from "firebase/firestore";
import { useCollectionData } from "react-firebase-hooks/firestore";
import { db } from "@/library/firebase/client";
import { DB_COLLECTIONS } from "@/library/constants";
import { createFirestoreDataConverter } from "@/library/firebase/helpers";
import { InvoiceTypes } from "@/library/@types/invoice";
const invoiceConverter = createFirestoreDataConverter<InvoiceTypes>();
const invoicesRef = collection(db, DB_COLLECTIONS.INVOICES);
const PAGE_SIZE = 6;
const useFetchInvoices = (cursor: DocumentSnapshot | undefined, per_page: number) => {
const constraints: QueryConstraint[] = [orderBy("created_at", "desc"), limit(per_page)];
if (cursor) constraints.push(startAfter(cursor));
const invoicesQuery = query(invoicesRef, ...constraints).withConverter(invoiceConverter);
return useCollectionData(invoicesQuery, { snapshotListenOptions: { includeMetadataChanges: true } });
};
type UsePaginatedInvoicesReturnType = {
meta: {
total: number;
total_pages: number;
current_page: number;
per_page: number;
};
invoices: InvoiceTypes[];
loading: boolean;
onPageChanged: (nextPage: number) => void;
canGoBack: boolean;
canGoNext: boolean;
};
/**
* Custom hook to manage paginated invoices.
*
* This hook provides functionality to fetch and manage paginated invoices,
* including metadata such as total count, total pages, current page, and
* items per page. It also handles pagination state and provides methods
* to navigate between pages.
*
* @returns {UsePaginatedInvoicesReturnType} An object containing:
* - `meta`: Metadata about the pagination state, including total count, total pages, current page, and items per page.
* - `invoices`: The current list of invoices for the current page.
* - `loading`: A boolean indicating whether the invoices are currently being loaded.
* - `onPageChanged`: A callback function to change the current page.
* - `canGoBack`: A boolean indicating whether the user can navigate to the previous page.
* - `canGoNext`: A boolean indicating whether the user can navigate to the next page.
*/
const usePaginatedInvoices = (): UsePaginatedInvoicesReturnType => {
const [meta, setMeta] = useState({ total: 0, total_pages: 0, current_page: 1, per_page: PAGE_SIZE });
const cursors = useRef<Map<number, DocumentSnapshot>>(new Map());
const [invoices = [], loading, _error, snapshot] = useFetchInvoices(cursors.current.get(meta.current_page), meta.per_page);
useEffect(() => {
getCountFromServer(query(invoicesRef)).then((result) => {
const count = result.data().count;
setMeta((meta) => ({ ...meta, total: count, total_pages: Math.ceil(count / meta.per_page) }));
});
}, []);
const onPageChanged = useCallback((nextPage: number) => {
setMeta((meta) => {
cursors.current.set(meta.current_page + 1, snapshot?.docs[snapshot.docs.length - 1] ?? ({} as DocumentSnapshot));
return { ...meta, current_page: nextPage };
});
}, [snapshot]);
const canGoBack = meta.current_page > 1;
const canGoNext = meta.current_page < meta.total_pages;
return { meta, invoices, loading, onPageChanged, canGoBack, canGoNext };
};
export { usePaginatedInvoices };
const InvoicePagination = () => {
const { meta, onPageChanged, canGoBack, canGoNext } = usePaginatedInvoices();
return (
<Pagination className="flex items-center space-x-2">
<PaginationContent>
<PaginationItem>
<PaginationPrevious disabled={!canGoBack} onClick={() => onPageChanged(meta.current_page - 1)}>
Previous
</PaginationPrevious>
</PaginationItem>
<PaginationItem>{meta.current_page}</PaginationItem>
<PaginationItem>of {meta.total_pages}</PaginationItem>
<PaginationItem>
<PaginationNext disabled={!canGoNext} onClick={() => onPageChanged(meta.current_page + 1)}>
Next
</PaginationNext>
</PaginationItem>
</PaginationContent>
</Pagination>
);
}
export default InvoicePagination;
Well done, please add ts after ticks to highlight your code.
```ts
// here comes code
https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting
Done
Well done, please add ts after ticks to highlight your code.
```ts // here comes code
https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting
Cool. I forgot to mention tsx option for files that also contain HTML.
Done
Cool. I forgot to mention tsx option for files that also contain HTML.
Question: is there any way to paginate through firebase list using this library?