useInfiniteScroll<T> function

InfiniteScrollResult<T> useInfiniteScroll<T>({
  1. required Future<PageResult<T>> fetcher(
    1. int page
    ),
  2. bool immediate = true,
})

Create an infinite scroll data source.

Optimized for "load more" patterns where items accumulate.

final feed = useInfiniteScroll<Post>(
  fetcher: (page) async {
    final data = await api.getFeed(page: page);
    return PageResult(items: data.posts, hasMore: data.hasMore);
  },
);

// In build():
Column([
  if (feed.initialLoading())
    Spinner()
  else
    ...feed.items().map((p) => PostCard(p)),

  if (feed.loading() && !feed.initialLoading())
    Spinner(),  // Loading more indicator

  if (feed.hasMore() && !feed.loading())
    IntersectionObserver(onVisible: feed.loadMore),
]);

Implementation

InfiniteScrollResult<T> useInfiniteScroll<T>({
  required Future<PageResult<T>> Function(int page) fetcher,
  bool immediate = true,
}) {
  final items = ref<List<T>>([]);
  final loading = ref(false);
  final initialLoading = ref(true);
  final error = ref<String?>(null);
  final hasMore = ref(true);
  final currentPage = ref(0);

  Future<void> loadMore() async {
    if (loading() || !hasMore()) return;

    loading.value = true;
    error.value = null;

    try {
      final nextPage = currentPage() + 1;
      final result = await fetcher(nextPage);

      items.value = [...items(), ...result.items];
      currentPage.value = nextPage;
      hasMore.value = result.hasMore;
    } catch (e) {
      error.value = e.toString();
    } finally {
      loading.value = false;
      initialLoading.value = false;
    }
  }

  Future<void> refresh() async {
    items.value = [];
    currentPage.value = 0;
    hasMore.value = true;
    initialLoading.value = true;
    await loadMore();
  }

  if (immediate) {
    loadMore();
  }

  return (
    items: items,
    loading: loading,
    initialLoading: initialLoading,
    error: error,
    hasMore: hasMore,
    loadMore: loadMore,
    refresh: refresh,
  );
}