Paginate with useCollection
#1332
Replies: 5 comments 9 replies
-
I admit I haven't used vuefire yet (will be doing so soon), but have you looked into an "infinite loading" component instead? Something like this one: https://vue3-infinite-loading.netlify.app/ In my case, I am using Nuxt 3 and handling the data fetching on the backend, but vue3/client-side data fetching should be fine too. Here's example of my data fetching from Firestore:
Hope this helps someone. |
Beta Was this translation helpful? Give feedback.
-
Would love to know if there's an elegant way for pagination. Its pretty basic so I feel it should be there, but I can't find anything in the docs. |
Beta Was this translation helpful? Give feedback.
-
I'm also looking for pagination examples. If anyone has examples to share please do! |
Beta Was this translation helpful? Give feedback.
-
After some more tweaking I came up with two other versions. These are much simpler, and primarily rely on the VueFire Hope it helps someone else. And if you have any improvements please share! VERSION 1 - Using Firestore Document Snapshot as a pagination cursor: The first version uses Firestore Document Snapshot as a pagination cursor so that we are only fetching a small batch of documents each time. This will save on document read costs. <template>
<div v-for="document in allDocs" :key="document.id">
<p>
{{ document.createdAt }}
</p>
</div>
<button @click="loadNextPage">Next</button>
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue";
import { useCollection, useFirestore } from "vuefire";
import {
doc,
collection,
getDoc,
limit,
orderBy,
query,
startAfter,
DocumentData,
} from "firebase/firestore";
const firestore = useFirestore();
const docsPerFetch = 1;
const collectionRef = collection(firestore, "myCollectionName");
const allDocs = ref<DocumentData[]>([]);
const lastVisibleDocSnap = ref();
// Build a computed collection query
const collectionQuery = computed(() => {
if (!lastVisibleDocSnap.value) {
// Default query
return query(
collectionRef,
orderBy("createdAt", "desc"),
limit(docsPerFetch)
);
} else {
// Paginated query
return query(
collectionRef,
orderBy("createdAt", "desc"),
startAfter(lastVisibleDocSnap.value),
limit(docsPerFetch)
);
}
});
const vueFireDocs = useCollection(collectionQuery);
// When vueFireDocs changes, push those new docs into the allDocs array
watch(vueFireDocs, () => {
allDocs.value = [...allDocs.value, ...vueFireDocs.value];
console.log(allDocs.value);
});
// When "Next" button is clicked change the lastVisibleDocSnap
// Because lastVisibleDocSnap is tracked in the computed collection query it triggers useCollection to get new docs
// Those new docs are then pushed into the allDocs array via the watch above
const loadNextPage = async () => {
const lastVisibleDoc = allDocs.value[allDocs.value.length - 1];
const lastVisibleDocRef = doc(firestore, "myCollectionName", lastVisibleDoc.id);
lastVisibleDocSnap.value = await getDoc(lastVisibleDocRef);
};
</script> VERSION 2 - Increase the limit counter: This second version just increases the So if you have a large set of documents, and you want to save on document read costs, then use version 1. Otherwise, version 2 is simpler. <template>
<div v-for="document in docs" :key="document.id">
<p>
{{ document.createdAt }}
</p>
</div>
<button @click="loadNextPage">Next</button>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import { useCollection, useFirestore } from "vuefire";
import { collection, limit, orderBy, query } from "firebase/firestore";
const firestore = useFirestore();
const collectionRef = collection(firestore, "myCollection");
const docsPerFetch = ref(1);
// Build a computed collection query
const collectionQuery = computed(() => {
return query(
collectionRef,
orderBy("createdAt", "desc"),
limit(docsPerFetch.value)
);
});
const docs = useCollection(collectionQuery);
const loadNextPage = async () => {
docsPerFetch.value = docsPerFetch.value + 1;
};
</script> |
Beta Was this translation helpful? Give feedback.
-
I have build utility function for this. I am still working on it, so its still very messy, but maybe it could help someone out there. You just initialize it and then update the pagination.page const { data: userList, pending: userListLoading, pagination } = createPagination({
collectionName: 'users',
}) export function createPagination<T>(payload: {
collectionName: string
collectionType?: 'collection' | 'collectionGroup'
orderBy?: { field: string; direction: 'asc' | 'desc' }[]
}) {
const { collectionName } = payload
const collectionType = payload.collectionType || 'collection'
const dynamicCollection = collectionType === 'collection' ? collection : collectionGroup
let orderByConstraint = [] as QueryOrderByConstraint[]
if (payload.orderBy) {
const _orderByConstraint = [] as QueryOrderByConstraint[]
payload.orderBy.forEach(order => {
_orderByConstraint.push(orderBy(order.field, order.direction))
})
orderByConstraint = _orderByConstraint
}
const pagination = ref({
itemsPerPage: 10,
page: 1,
totalItems: 0,
queryId: undefined as string | undefined,
options: undefined as 'startBefore' | 'endBefore' | 'startAt' | undefined,
orderByArr: orderByConstraint,
})
function resetPagination({ orderByArr }: { fieldFilterArr?: QueryFieldFilterConstraint[]; orderByArr?: QueryOrderByConstraint[] } = {}) {
pagination.value = {
itemsPerPage: 10,
page: 1,
totalItems: 0,
queryId: undefined,
options: undefined,
orderByArr: orderByArr || [],
}
}
async function getPaginatedQuery() {
const { itemsPerPage, queryId, options } = pagination.value
const queryFilters = [] as QueryConstraint[]
const countQuery = query(dynamicCollection(db, collectionName), ...queryFilters)
const countSnap = await getCountFromServer(countQuery)
pagination.value.totalItems = countSnap.data().count
if (!queryId) {
return query(dynamicCollection(db, collectionName), limit(itemsPerPage))
}
else {
const lastDocSnap = await getDoc(doc(db, collectionName, queryId))
const filters = queryFilters
let optionsQ: QueryConstraint[] = []
if (options === 'startBefore')
optionsQ = [...filters, startAfter(lastDocSnap), limit(itemsPerPage)]
if (options === 'endBefore')
optionsQ = [...filters, limitToLast(itemsPerPage), endBefore(lastDocSnap)]
if (options === 'startAt')
optionsQ = [...filters, startAt(lastDocSnap), limit(itemsPerPage)]
return query(dynamicCollection(db, collectionName), orderBy('createdDate', 'desc'), ...optionsQ)
}
}
const lisQuery = computedAsync(getPaginatedQuery)
const { pending, data } = useCollection<T>(lisQuery)
// Cater for paging between pages
watch(() => pagination.value.page, async (newPage, oldPage) => {
if (newPage === oldPage)
return
if (newPage === 1) {
pagination.value = {
...pagination.value,
queryId: undefined,
options: undefined,
}
return
}
if (newPage > oldPage) {
pagination.value = {
...pagination.value,
queryId: data.value[data.value.length - 1].id,
options: 'startBefore',
}
}
if (newPage < oldPage) {
pagination.value = {
...pagination.value,
queryId: data.value[0].id,
options: 'endBefore',
}
}
})
return {
pagination,
resetPagination,
pending,
data,
}
} |
Beta Was this translation helpful? Give feedback.
-
I'm trying create a pagination with a collection with this:
But I don't feel this is fancy. There is a better way to do this?
Beta Was this translation helpful? Give feedback.
All reactions