newOS for Developers
Back to Overview
useScrollItems
hook108 linesImplements infinite scroll pagination with viewport detection, debounced loading, and support for reversed lists (chat-style).
Source Path
apps/web/hooks/useScrollItems.tsPackage
@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 pageKey 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.