usePaginatedQuery<T> function

PaginatedValue<QuerySnapshot<T>> usePaginatedQuery<T>({
  1. required Query<T> query,
  2. required Object orderBy,
  3. bool descending = false,
  4. int pageLimit = 10,
  5. int limit = 10,
  6. bool listen = false,
  7. GetOptions? getOptions,
  8. bool includeMetadataChanges = false,
  9. bool preserveState = true,
})

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,
  );
}