Skip to main content

Infinite Scroll

Infinite scroll is a useRemoteDataMap where each page is a key and you grow the list of keys as the user scrolls.

const [cursors, setCursors] = useState([0]);
const pages = useRemoteDataMap<number, Page>((cursor, signal) => fetchPage(cursor, signal));

Each cursor maps to an independent store. Already-loaded pages stay rendered while the next page loads. If page 3 fails, pages 1 and 2 keep their data; the error and retry button only appear for page 3.

Try it

This snippet simulates 25 items across 5 pages, with random failures on roughly every 4th fetch. Click "Load more" to fetch the next page. When a page fails, click "Retry this page"; only the failed page re-fetches.

import { useState } from 'react';
import { Await, useRemoteDataMap } from 'use-remote-data';

// Simulated API — returns 5 items per page and a next cursor
interface Page {
items: string[];
nextCursor: number | null;
}

let fetchCount = 0;

const fetchPage = (cursor: number): Promise<Page> =>
new Promise((resolve, reject) => {
setTimeout(() => {
fetchCount++;
// Simulate a random failure on ~every 4th fetch
if (fetchCount % 4 === 0) {
reject(new Error('Network error'));
return;
}
const items = Array.from({ length: 5 }, (_, i) => {
const n = cursor + i + 1;
return `Item #${n}`;
});
const nextCursor = cursor + 5 < 25 ? cursor + 5 : null;
resolve({ items, nextCursor });
}, 600);
});

export function Component() {
const [cursors, setCursors] = useState([0]);
const pages = useRemoteDataMap<number, Page>((cursor) => fetchPage(cursor));

const loadMore = (nextCursor: number) =>
setCursors((prev) =>
prev.includes(nextCursor) ? prev : [...prev, nextCursor]
);

return (
<div>
{cursors.map((cursor) => (
<Await
key={cursor}
store={pages.get(cursor)}
loading={() => (
<p style={{ color: 'gray' }}>Loading page...</p>
)}
error={({ retry }) => (
<div style={{ color: 'red' }}>
<p>Failed to load page.</p>
<button onClick={retry}>Retry this page</button>
</div>
)}
>
{(page) => (
<>
{page.items.map((item) => (
<p key={item}>{item}</p>
))}
{page.nextCursor !== null && (
<button
onClick={() => loadMore(page.nextCursor!)}
>
Load more
</button>
)}
</>
)}
</Await>
))}
</div>
);
}

How it works

  1. useState([0]) tracks which cursors to render; starts with the first page
  2. useRemoteDataMap manages one store per cursor; fetches are deduped by key
  3. Each <Await> independently handles loading, error, and success for its page
  4. "Load more" appends the next cursor to the list, which triggers a new <Await> to mount and start fetching
  5. A failed page shows its own error + retry without affecting the rest

Compared to react-query

react-query has a dedicated useInfiniteQuery hook with built-in page management and cursor tracking. The useRemoteDataMap approach is simpler (no special hook, no getNextPageParam config), but you manage the cursor list yourself with useState.