codedsaif-react-hooks
Version:
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
125 lines • 5.72 kB
JavaScript
import { useState, useEffect, useCallback } from "react";
/**
* useInfiniteScroll
* Supports both automatic callback fetching and manual slicing with scroll.
* @param {Function|null} callback - Called when scroll reaches bottom (set null for manual mode)
* @param {Object} options - Configuration options
* @param {React.RefObject} [options.containerRef=null] - Scrollable container ref
* @param {boolean} [options.manual=false] - If true, enables manual mode and returns 'limit'
* @param {number} [options.start=10] - Initial limit in manual mode
* @param {number} [options.pace=10] - Increment step for limit
* @param {number} [options.offset=100] - Distance from bottom to trigger scroll (in px)
* @param {boolean} [options.hasMore=true] - Set false to stop infinite loading when no more data
* @param {boolean} [options.debug=false] - Enable funny console logs
*
* @returns {[boolean, Function]|number}
* - If manual=false: returns [isFetching, setIsFetching]
* - If manual=true: returns limit (number)
*
* Example: Auto-fetch Mode (Default)
* -------------------------------------
* const containerRef = useRef(null);
* const [data, setData] = useState([]);
* const [page, setPage] = useState(1);
* const [hasMore, setHasMore] = useState(true);
* const fetchMore = async () => {
* const res = await fetch(`https://medrum.herokuapp.com/feeds/?source=5718e53e7a84fb1901e05971&page=${page}&sort=latest`);
* const json = await res.json();
* if (json.length === 0) setHasMore(false);
* // OR if(json.count === data.length) setHasMore(false);
* else {
* setData(prev => [...prev, ...json]);
* setPage(prev => prev + 1);
* }
* };
* const [isFetching, setIsFetching] = useInfiniteScroll(fetchMore, { containerRef, hasMore });
*
* Example: Manual Slice Mode
* ------------------------------
* const containerRef = useRef(null);
* const [data, setData] = useState([]);
* const [page, setPage] = useState(1);
* const [hasMore, setHasMore] = useState(true);
* const limit = useInfiniteScroll(null, { containerRef, manual: true, start: 10, pace: 10, hasMore });
* const fetchMore = async () => {
* const res = await fetch(`https://medrum.herokuapp.com/feeds/?source=5718e53e7a84fb1901e05971&page=${page}&sort=latest`);
* const json = await res.json();
* if (json.length === 0) setHasMore(false);
* else {
* setData(prev => [...prev, ...json]);
* setPage(prev => prev + 1);
* }
* };
* useEffect(() => {
* fetchMore();
* }, [limit]);
* return (
* <ul ref={containerRef} style={{ height: "400px", overflowY: "scroll" }}>
* {data.slice(0, limit).map(item => (
* <li key={item.id}>{item.title}</li>
* ))}
* </ul>
* );
*/
const useInfiniteScroll = (
callback,
{ containerRef = null, manual = false, start = 10, pace = 10, offset = 10, hasMore = true, debug = false, } = {}
) => {
const [isFetching, setIsFetching] = useState(false);
const [limit, setLimit] = useState(start);
const handleScroll = useCallback(() => {
const el = containerRef?.current || document.documentElement;
if (debug) console.log("[🌀 useInfiniteScroll] Scroll event fired...");
const scrollTop = el?.scrollTop || window.scrollY;
const scrollHeight = el?.scrollHeight || document.body.scrollHeight;
const clientHeight = el?.clientHeight || window.innerHeight;
const nearBottom = clientHeight + scrollTop >= scrollHeight - offset;
if (debug) {
console.log("[🌀 useInfiniteScroll] Scroll event fired...");
console.log(
`[🔍 Scroll Check] scrollTop: ${scrollTop}, clientHeight: ${clientHeight}, scrollHeight: ${scrollHeight}, nearBottom: ${nearBottom}`
);
}
if (nearBottom && !isFetching && hasMore) {
if (debug) console.log("[🚀 Trigger] Threshold hit. Loading more...");
if (manual) {
setLimit((prev) => {
if (debug) console.log("[📦 Manual Mode] Increasing limit...");
return prev + pace;
});
} else {
setIsFetching(true);
}
} else {
if (debug) {
console.log("[❌ Ignored Scroll] Conditions not met:");
console.table({ nearBottom, isFetching, hasMore });
}
}
}, [containerRef, isFetching, hasMore, manual, offset, pace, debug]);
useEffect(() => {
const el = containerRef?.current || window;
if (debug) console.log("📌 Binding scroll event on", el === window ? "window" : "custom container");
el.addEventListener("scroll", handleScroll);
return () => {
if (debug) console.log("[🔌 useInfiniteScroll] Detaching scroll listener...");
el.removeEventListener("scroll", handleScroll);
};
}, [handleScroll, containerRef, debug]);
useEffect(() => {
if (!manual && isFetching) {
if (debug) console.log("[🔄 useInfiniteScroll] Calling fetch callback...");
const maybePromise = callback?.();
if (maybePromise?.finally) {
maybePromise.finally(() => {
if (debug) console.log("[✅ useInfiniteScroll] Fetch complete!");
setIsFetching(false);
});
} else {
setIsFetching(false);
}
}
}, [isFetching, manual, callback, debug]);
return manual ? limit : [isFetching, setIsFetching];
};
export default useInfiniteScroll;