Skip to content
Sensors

useInfiniteScroll

Triggers a load callback whenever the scroll position reaches a boundary of a scrollable element, enabling infinite scroll for lists and feeds. Supports all four scroll directions, pre-load distance, manual control, async callbacks, and a canLoadMore gate function to control when loading stops.

useInfiniteScroll
Idle

Scroll to the bottom of the list to load more items.

Items
10
Max
50
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10

Call useInfiniteScroll with a ref to the scrollable element and an async onLoadMore callback. The callback fires automatically when the user scrolls to the bottom (default direction).

import {
const useRef$: {
<T = any>(initialValue: null): Ref$<T | null>;
<T = any>(initialValue: T): Ref$<T>;
<T = any>(): Ref$<T | null>;
}
useRef$
} from "@usels/core";
import {
const useInfiniteScroll: (element: MaybeEventTarget<Element | Document | Window>, onLoadMore: (direction: UseInfiniteScrollDirection) => Awaitable<void>, options?: DeepMaybeObservable<UseInfiniteScrollOptions>) => UseInfiniteScrollReturn
useInfiniteScroll
} from "@usels/web";
function
function Component(): JSX.Element
Component
() {
const
const el$: Ref$<HTMLDivElement | null>
el$
=
useRef$<HTMLDivElement>(): Ref$<HTMLDivElement | null> (+2 overloads)

Core (framework-agnostic) version of useRef$. Creates an observable element ref with opaque wrapping.

  • non-null value → Ref$<T>: current, get(), peek() return T
  • null / no arg → Ref$<T | null>: current, get(), peek() return T | null

Nullability is expressed via the type parameter, mirroring T | null at the call site.

useRef$
<
interface HTMLDivElement
HTMLDivElement
>();
const {
const isLoading$: ReadonlyObservable<boolean>

Currently loading

isLoading$
} =
function useInfiniteScroll(element: MaybeEventTarget<Element | Document | Window>, onLoadMore: (direction: UseInfiniteScrollDirection) => Awaitable<void>, options?: DeepMaybeObservable<UseInfiniteScrollOptions>): UseInfiniteScrollReturn

Framework-agnostic infinite scroll core.

Triggers onLoadMore whenever the scroll position reaches the configured boundary of a scrollable element. Composes createScroll and createElementVisibility. Must be called inside a useScope factory.

useInfiniteScroll
(
const el$: Ref$<HTMLDivElement | null>
el$
, async () => {
const
const newItems: any
newItems
= await
any
fetchNextPage
();
any
items
.
any
push
(...
const newItems: any
newItems
);
});
return (
<
JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
RefAttributes<HTMLDivElement>.ref?: Ref<HTMLDivElement> | undefined

Allows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref).

ref
={
const el$: Ref$<HTMLDivElement | null>
el$
}
HTMLAttributes<HTMLDivElement>.style?: CSSProperties | undefined
style
={{
StandardLonghandProperties<string | number, string & {}>.height?: Property.Height<string | number> | undefined

This feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.

Syntax: auto | <length-percentage [0,∞]> | min-content | max-content | fit-content | fit-content(<length-percentage [0,∞]>) | <calc-size()> | <anchor-size()>

Initial value: auto

| Chrome | Firefox | Safari | Edge | IE | | :----: | :-----: | :----: | :----: | :---: | | 1 | 1 | 1 | 12 | 4 |

height
: 300,
StandardLonghandProperties<string | number, string & {}>.overflowY?: Property.OverflowY | undefined

This feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.

Syntax: visible | hidden | clip | scroll | auto

Initial value: visible

| Chrome | Firefox | Safari | Edge | IE | | :----: | :-----: | :----: | :----: | :---: | | 1 | 3.5 | 3 | 12 | 5 |

overflowY
: "auto" }}>
{/* list items */}
{
const isLoading$: ReadonlyObservable<boolean>

Currently loading

isLoading$
.
ImmutableObservableBase<boolean>.get(trackingType?: TrackingType | GetOptions): {}
get
() && <
JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>Loading…</
JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>}
</
JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
);
}

Use the direction option to trigger loading from any edge. Certain directions require specific CSS on the scrollable container so that new content prepends in the correct visual position.

DirectionRequired CSS
bottom (default)No special settings required
topdisplay: flex; flex-direction: column-reverse;
leftdisplay: flex; flex-direction: row-reverse;
rightdisplay: flex;
useInfiniteScroll(
el$,
async () => {
/* load older messages */
},
{ direction: "top" }
);

Set distance (in px) to start loading before the user actually reaches the boundary — useful for pre-fetching the next page early.

useInfiniteScroll(el$, onLoadMore, {
distance: 200, // trigger 200px before the bottom edge
});

Pass a canLoadMore function to control whether loading should trigger. When it returns false, the scroll listener is skipped entirely. Use this to stop loading once all pages have been fetched.

useInfiniteScroll(
el$,
async () => {
const page = await fetchNextPage();
if (!page.hasMore) setHasMore(false);
},
{
canLoadMore: () => hasMore,
}
);

Use the interval option to set a minimum number of milliseconds between consecutive load triggers.

useInfiniteScroll(el$, onLoadMore, {
interval: 200, // minimum 200ms between consecutive loads (default: 100)
});

load triggers a load imperatively; reset re-checks the current scroll position and triggers a load if the boundary condition is met.

const { load, reset, isLoading$ } = useInfiniteScroll(el$, async () => {
await fetchNextPage();
});
ParameterTypeDescription
elementMaybeEventTarget<Element | Document | Window>-
onLoadMore(direction: UseInfiniteScrollDirection) => Awaitable<void>-
optionsUseInfiniteScrollOptions (optional)-
OptionTypeDefaultDescription
directionUseInfiniteScrollDirection-Load trigger direction. Default: “bottom”
distancenumber-arrivedState offset in px. Default: 0
immediateboolean-Whether to auto-load on mount if content is already at boundary. Default: true
intervalnumber-Minimum ms between consecutive load triggers. Default: 100
canLoadMore((el: HTMLElement) => boolean)-Gate function — return false to block loading. Evaluated before each load.
scrollOptionsUseScrollOptions-Additional options passed to useScroll
windowWindowSource--

UseInfiniteScrollReturn

NameTypeDescription
isLoading$ReadonlyObservable<boolean>Currently loading
load() => Promise<void>Manually trigger a load
reset() => voidRe-check scroll position and trigger load if needed

View on GitHub