For AI agents: the complete documentation index is available at https://ifhover.github.io/leaf-ui/en/llms.txt, the full documentation bundle is available at https://ifhover.github.io/leaf-ui/en/llms-full.txt, and this page is available as Markdown at https://ifhover.github.io/leaf-ui/en/components/infinite-scroll.md.

InfiniteScroll

Load more content when reaching the end of a list.

Basic usage

Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
basic.tsx
import { InfiniteScroll, ScrollArea } from '@ifhover/leaf-ui';

import { useRef, useState } from 'react';

export function InfiniteScrollBasic() {
  const root = useRef<HTMLDivElement>(null);
  const [items, setItems] = useState(() => Array.from({ length: 10 }, (_, i) => i));
  return (
    <ScrollArea ref={root} height={240} aria-label="Growing list">
      <InfiniteScroll
        dataLength={items.length}
        hasMore={items.length < 40}
        target={() => root.current}
        onLoadMore={async (signal) => {
          await new Promise((resolve) => setTimeout(resolve, 500));
          if (!signal.aborted)
            setItems((previous) => [
              ...previous,
              ...Array.from({ length: 10 }, (_, index) => previous.length + index),
            ]);
        }}
      >
        {items.map((item) => (
          <div key={item} style={{ padding: '12px 16px' }}>
            Item {item + 1}
          </div>
        ))}
      </InfiniteScroll>
    </ScrollArea>
  );
}

More examples

Record 1

Record 2

Record 3

Record 4

Record 5

Record 6

advanced.tsx
import { InfiniteScroll, ScrollArea } from '@ifhover/leaf-ui';

import { useRef, useState } from 'react';

export function InfiniteScrollRetry() {
  const root = useRef<HTMLDivElement>(null);
  const attempt = useRef(0);
  const [items, setItems] = useState([0, 1, 2, 3, 4, 5]);
  return (
    <ScrollArea ref={root} height={180} aria-label="Retry example">
      <InfiniteScroll
        dataLength={items.length}
        hasMore={items.length < 12}
        target={() => root.current}
        onLoadMore={async (signal) => {
          await new Promise((resolve) => setTimeout(resolve, 300));
          if (signal.aborted) return;
          if (attempt.current++ === 0) throw new Error('Retry demo');
          setItems((previous) => [...previous, ...[6, 7, 8, 9, 10, 11]]);
        }}
      >
        {items.map((item) => (
          <p key={item}>Record {item + 1}</p>
        ))}
      </InfiniteScroll>
    </ScrollArea>
  );
}

Usage

Return a Promise from onLoadMore; requests do not overlap. Update dataLength after appending. The signal aborts on unmount and failures can be retried.

Types

Import these types for data, callbacks and refs.

import type { InfiniteScrollProps } from '@ifhover/leaf-ui';

InfiniteScrollProps API

PropertyTypeDefaultDescription
dataLengthnumberRequiredLoaded count; changes allow the next request
hasMorebooleanRequiredMore data remains
onLoadMore(signal: AbortSignal) => Promise<void>RequiredLoad more; signal aborts the request
target() => HTMLElement | null—Scroll target or link target
thresholdnumber200Pixels from the end that trigger loading
loaderReactNode—Loading placeholder
endMessageReactNode—Content after all items are loaded
onError(error: unknown) => void—Error callback