newOS for Developers

Back to Overview

useScrollItems

hook108 lines

Implements infinite scroll pagination with viewport detection, debounced loading, and support for reversed lists (chat-style).

Source Path
apps/web/hooks/useScrollItems.ts
Package

@newos/web

Signature

useScrollItems<T>({
  items?: T[];                              // Full list of items
  progress?: ProgressEntrySignal;           // Progress from useProgressiveLiveQuery
  reverse?: boolean;                        // Reverse order (for chat)
}): [
  displayItems: T[],                        // Paginated items to render
  sentinelRef: React.RefObject<any>,        // Attach to scroll sentinel element
  loadMore: () => void,                     // Manual load trigger
  page: number,                             // Current page number
  hasMore: boolean                          // More items available?
]

Constants

const PAGE_SIZE = 20;  // Items per page

Key Features

Viewport Detection

Uses useInViewport from ahooks with 0.5 threshold. Automatically loads more when sentinel element is visible.

Debounced Loading

loadMore is debounced with 2000ms wait and leading trigger. Page state is debounced with 200ms trailing.

Reversed Mode

When reverse=true, items are displayed newest-first (ideal for chat). Uses slice().reverse() internally.

Progressive Handler Integration

Works with ProgressEntrySignal from newgraph-signals. Calls progress.continue() to fetch next page.

Auto-Load More

Automatically triggers extra page load if initial content has fewer than 10 non-card items (horizontal scroll fix).

Reset Function

Internal reset() resets pagination and callsprogress.reset() for full refresh.

Usage Example

import { useScrollItems } from "@/hooks/useScrollItems";
import { useProgressiveLiveQuery } from "@/hooks/useProgressiveLiveQuery";

function ChatView({ folderId }) {
  const [posts, progress] = useProgressiveLiveQuery(
    () => readPosts({ folderId }),
    [folderId]
  );

  const [displayItems, sentinelRef, loadMore, page, hasMore] = useScrollItems({
    items: posts,
    progress,
    reverse: true,  // Newest at bottom for chat
  });

  return (
    <div style={{ display: "flex", flexDirection: "column-reverse" }}>
      {displayItems.map((post) => (
        <PostWidget key={post.id} post={post} />
      ))}
      
      {/* Sentinel for infinite scroll */}
      <div ref={sentinelRef}>
        {hasMore && progress?.value.inProgress && <Spinner />}
      </div>
      
      {/* Optional manual load button */}
      {hasMore && !progress?.value.inProgress && (
        <button onClick={loadMore}>Load More</button>
      )}
    </div>
  );
}

Edge Cases & Gotchas

401 Error Handling — If progress.error.status === 401, loading stops (unauthorized user). Handle login redirect separately.
hasMore Logic — Set to false whendisplayItems.length === items.length AND progress.done.
Card Content Filter — The auto-load-more check filters out items with content.includes('/card'). These are embed/card posts.
Sentinel Visibility — The sentinel ref must be attached to a visible element for viewport detection to work. Use at list end.
Dependencies — Relies on ahooks foruseDebounce, useDebounceFn, and useInViewport.

Related

useProgressiveLiveQueryFolderCoreuseInViewport (ahooks)useDebounceFn (ahooks)