use-simple-infinite-scroll
v1.1.0
Published
A simple React Hook for infinite scrolling built on the Intersection Observer API
Downloads
21
Readme
Installation
npm install use-simple-infinite-scroll
Usage
Basic
import React, { useState } from 'react';
import { useSimpleInfiniteScroll } from 'use-simple-infinite-scroll';
type Item = {
id: number;
name: string;
};
type Result = {
data: Item[];
nextCursor: number | null;
};
const canFetchMore = (nextCursor: Result['nextCursor']) => nextCursor !== null;
const InfiniteScrollExample = () => {
const [isLoading, setIsLoading] = useState(false);
const [items, setItems] = useState<Item[]>([]);
const [nextCursor, setNextCursor] = useState<Result['nextCursor']>(0);
const fetchMore = () => {
setIsLoading(true);
fetch(`/api/items?cursor=${nextCursor}`)
.then((res) => res.json())
.then((res: Result) => {
setItems([...items, ...res.data]);
setNextCursor(res.nextCursor);
setIsLoading(false);
});
};
const [targetRef] = useSimpleInfiniteScroll({
onLoadMore: fetchMore,
canLoadMore: canFetchMore(nextCursor),
});
return (
<>
{items.length !== 0 ? (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
) : null}
<div ref={targetRef}>
{isLoading
? 'Loading more...'
: canFetchMore(nextCursor)
? 'Load More'
: 'Nothing more to load'}
</div>
</>
);
};
React Query
import React from 'react';
import { useInfiniteQuery } from 'react-query';
import { useSimpleInfiniteScroll } from 'use-simple-infinite-scroll';
type Item = {
id: number;
name: string;
};
type Result = {
data: Item[];
nextCursor: number | null;
};
const InfiniteScrollExample = () => {
const {
status,
data,
error,
isFetching,
isFetchingMore,
fetchMore,
canFetchMore,
} = useInfiniteQuery<Result, Error>(
'items',
(key: string, cursor = 0) =>
fetch(`/api/items?cursor=${cursor}`).then((res) => res.json()),
{
getFetchMore: (lastGroup) => lastGroup.nextCursor,
},
);
const [targetRef, rootRef] = useSimpleInfiniteScroll({
onLoadMore: fetchMore,
canLoadMore: !!canFetchMore,
});
return status === 'loading' ? (
<p>Loading...</p>
) : status === 'error' ? (
<span>Error: {error && error.message}</span>
) : (
<div
style={{
overflow: 'auto',
}}
ref={rootRef}
>
<ul>
{data &&
data.map((page, i) => (
<React.Fragment key={i}>
{page.data.map((item) => (
<li key={itme.id}>{item.name}</li>
))}
</React.Fragment>
))}
</ul>
<div>
<button
type="button"
ref={targetRef}
onClick={() => fetchMore()}
disabled={!canFetchMore || !!isFetchingMore}
>
{isFetchingMore
? 'Loading more...'
: canFetchMore
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
<div>
{isFetching && !isFetchingMore ? 'Background Updating...' : null}
</div>
</div>
);
};
API
const useSimpleInfiniteScroll: (options: {
canLoadMore: boolean;
onLoadMore: () => void;
rootMargin?: string;
threshold?: number | number[];
}) => [(target: Element | null) => void, (root: Element | null) => void];
| Name | Type | Default | Required | Descripttion |
| :------------ | :------------------- | :------ | :------: | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
| canLoadMore
| boolean
| | ✓ | Specifies if there are more entities to load. |
| onLoadMore
| () => void
| | ✓ | Called when the user has scrolled all the way to the end. |
| rootMargin
| string
| "0px"
| | Margin around the root. Can have values similar to the CSS margin property, e.g. "10px 20px 30px 40px"
(top, right, bottom, left). |
| threshold
| number \| number[]
| 0
| | Either a single number or an array of numbers which indicate at what percentage of the target's visibility the observer's callback should be executed. |
For more information on rootMargin
and threshold
option, visit the MDN web docs.
FAQ
How can I assign multiple refs to a component?
You can wrap multiple ref
assignments in a single useCallback
:
import React, { useRef, useCallback } from 'react';
import { useSimpleInfiniteScroll } from 'use-simple-infinite-scroll';
const AssignMultipleRefsExample = () => {
const rootRef = useRef<HTMLDivElement | null>();
const targetRef = useRef<HTMLDivElement | null>();
const [setTargetRef, setRootRef] = useSimpleInfiniteScroll({
onLoadMore: () => {},
canLoadMore: true,
});
// Use `useCallback` so we don't recreate the function on each render - Otherwise, the function passed to `onLoadMore` option will be called twice
const setRootRefs = useCallback(
(node: HTMLDivElement | null) => {
// Ref's from useRef needs to have the node assigned to `current`
rootRef.current = node;
// Callback refs, like the one from `useSimpleInfiniteScroll`, is a function that takes the node as an argument
setRootRef(node);
},
[setRootRef],
);
const setTargetRefs = useCallback(
(node: HTMLDivElement | null) => {
targetRef.current = node;
setTargetRef(node);
},
[setTargetRef],
);
return (
<div ref={setRootRefs}>
<div ref={setTargetRefs} />
</div>
);
};
Which browsers are supported?
use-simple-infinite-scroll supports all of the major modern browsers. Browsers like IE11 are not supported: if you need to support older browsers you can add IntersectionObserver polyfill.
You can install the polyfill via npm or by downloading a zip of this repository:
npm install intersection-observer
Then import it in your app:
import 'intersection-observer';
Contributing
Contributions are always welcome! Please read the contributing first.
Contributors
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!