usePaginatedQuery<T> function
PaginatedValue<QuerySnapshot<T> >
usePaginatedQuery<T>({})
A paginated hook that will return a PaginatedValue
with the data from the
Firestore
query. The Firestore
query will be executed when the hook is
called. Providing a limit
will limit the number of documents returned.
Passing a listen
of true will cause the PaginatedValue
to listen to the
Firestore
query via snapshots.
Implementation
PaginatedValue<QuerySnapshot<T>> usePaginatedQuery<T>({
required Query<T> query,
required Object orderBy,
bool descending = false,
int pageLimit = 10,
int limit = 10,
bool listen = false,
GetOptions? getOptions,
bool includeMetadataChanges = false,
bool preserveState = true,
}) {
final futures = useState(<Future<QuerySnapshot<T>>>[]);
final streams = useState(<Stream<QuerySnapshot<T>>>[]);
final hasNext = useState(true);
final hasPrevious = useState(true);
final streamSubscription = useState<StreamSubscription?>(null);
final lastDocumentSnapshot = useRef<QueryDocumentSnapshot?>(null);
final orderedQuery = query.orderBy(orderBy, descending: descending);
Future<void> nextPage() async {
if (!hasNext.value) return;
final completer = Completer<void>();
Query<T> _query;
final _lastDocumentSnapshot = lastDocumentSnapshot.value;
if (_lastDocumentSnapshot != null) {
_query =
orderedQuery.limit(limit).startAfterDocument(_lastDocumentSnapshot);
} else {
_query = orderedQuery.limit(limit);
}
if (listen) {
final stream =
_query.snapshots(includeMetadataChanges: includeMetadataChanges);
streamSubscription.value?.cancel();
streamSubscription.value = stream.listen((snapshot) {
if (snapshot.docs.isEmpty) {
hasNext.value = false;
} else {
lastDocumentSnapshot.value = snapshot.docs.last;
}
if (!completer.isCompleted) {
completer.complete();
}
}, onError: (Object error, StackTrace? stackTrace) {
if (!completer.isCompleted) {
completer.completeError(error, stackTrace);
}
});
streams.value = [...streams.value, stream];
if (streams.value.length > pageLimit) {
// remove first page
streams.value.removeAt(0);
}
} else {
final future = _query.get(getOptions)
..then((snapshot) {
if (snapshot.docs.isEmpty) {
hasNext.value = false;
} else {
lastDocumentSnapshot.value = snapshot.docs.last;
}
// ignore: unnecessary_lambdas
}).catchError((Object error, StackTrace? stackTrace) {
completer.completeError(error, stackTrace);
});
futures.value = [...futures.value, future];
if (futures.value.length > pageLimit) {
futures.value.removeAt(0);
}
}
return completer.future;
}
Future<void> previousPage() async {
if (!hasPrevious.value) return;
final completer = Completer<void>();
Query<T> _query;
final _lastDocumentSnapshot = lastDocumentSnapshot.value;
if (_lastDocumentSnapshot != null) {
_query = orderedQuery
.limitToLast(limit)
.startAtDocument(_lastDocumentSnapshot);
} else {
_query = orderedQuery.limit(limit);
}
if (listen) {
final stream =
_query.snapshots(includeMetadataChanges: includeMetadataChanges);
streamSubscription.value?.cancel();
streamSubscription.value = stream.listen((snapshot) {
if (snapshot.docs.isEmpty) {
hasPrevious.value = false;
} else {
lastDocumentSnapshot.value = snapshot.docs.last;
}
if (!completer.isCompleted) {
completer.complete();
}
}, onError: (Object error, StackTrace? stackTrace) {
if (!completer.isCompleted) {
completer.completeError(error, stackTrace);
}
});
streams.value = [stream, ...streams.value];
if (streams.value.length > pageLimit) {
// remove last page
streams.value = streams.value.sublist(0, pageLimit);
}
} else {
final future = _query.get(getOptions)
..then((snapshot) {
if (snapshot.docs.isEmpty) {
hasPrevious.value = false;
} else {
lastDocumentSnapshot.value = snapshot.docs.last;
}
// ignore: unnecessary_lambdas
}).catchError((Object error, StackTrace? stackTrace) {
completer.completeError(error, stackTrace);
});
futures.value = [future, ...futures.value];
if (futures.value.length > pageLimit) {
futures.value = futures.value.sublist(0, pageLimit);
}
}
return completer.future;
}
final snapshots = <AsyncSnapshot<QuerySnapshot<T>>>[];
// Diff here to only call useStream on the ones I want I think
for (final future in futures.value) {
snapshots.add(
useFuture(future),
);
}
for (final stream in streams.value) {
snapshots.add(
useStream(stream),
);
}
useEffect(
() {
if (listen) {
if (streams.value.isNotEmpty) {
nextPage();
}
} else {
if (futures.value.isNotEmpty) {
nextPage();
}
}
},
[],
);
return PaginatedValue(
snapshots: snapshots,
nextPage: nextPage,
previousPage: previousPage,
hasNext: hasNext.value,
hasPrevious: hasPrevious.value,
);
}