Last active
September 14, 2024 09:27
-
-
Save ptenteromano/e42fe33622a26ba3fdd49b51109203c7 to your computer and use it in GitHub Desktop.
Infinite Scroll with Remix Run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Infinite Scroll using Remix Run | |
* Based on client-side Scroll position | |
* Full Article here: https://dev.to/ptenteromano/infinite-scroll-with-remix-run-1g7 | |
*/ | |
import { useEffect, useState, useCallback } from "react"; | |
import { LoaderFunction, useLoaderData, useFetcher } from "remix"; | |
import { fetchPhotos } from "~/utils/api/restful"; | |
import type { PhotoHash } from "~/utils/api/types"; | |
// Pull page down from the loader's api request | |
const getPage = (searchParams: URLSearchParams) => | |
Number(searchParams.get("page") || "1"); | |
export const loader: LoaderFunction = async ({ request }) => { | |
const page = getPage(new URL(request.url).searchParams); | |
const resp = await fetchPhotos(page); | |
return resp.photos; | |
}; | |
export default function Photos() { | |
const initialPhotos = useLoaderData<PhotoHash[]>(); | |
const [photos, setPhotos] = useState<PhotoHash[]>(initialPhotos); | |
const fetcher = useFetcher(); | |
const [scrollPosition, setScrollPosition] = useState(0); | |
const [clientHeight, setClientHeight] = useState(0); | |
const [height, setHeight] = useState(null); | |
const [shouldFetch, setShouldFetch] = useState(true); | |
const [page, setPage] = useState(2); | |
// Set the height of the parent container whenever photos are loaded | |
const divHeight = useCallback( | |
(node) => { | |
if (node !== null) { | |
setHeight(node.getBoundingClientRect().height); | |
} | |
}, | |
[photos.length] | |
); | |
// Add Listeners to scroll and client resize | |
useEffect(() => { | |
const scrollListener = () => { | |
setClientHeight(window.innerHeight); | |
setScrollPosition(window.scrollY); | |
}; | |
// Avoid running during SSR | |
if (typeof window !== "undefined") { | |
window.addEventListener("scroll", scrollListener); | |
} | |
// Clean up | |
return () => { | |
if (typeof window !== "undefined") { | |
window.removeEventListener("scroll", scrollListener); | |
} | |
}; | |
}, []); | |
// Listen on scrolls. Fire on some self-described breakpoint | |
useEffect(() => { | |
if (!shouldFetch || !height) return; | |
if (clientHeight + scrollPosition + 100 < height) return; | |
fetcher.load(`/photos?index&page=${page}`); | |
setShouldFetch(false); | |
}, [clientHeight, scrollPosition, fetcher]); | |
// Merge photos, increment page, and allow fetching again | |
useEffect(() => { | |
// Discontinue API calls if the last page has been reached | |
if (fetcher.data && fetcher.data.length === 0) { | |
setShouldFetch(false); | |
return; | |
} | |
// Photos contain data, merge them and allow the possiblity of another fetch | |
if (fetcher.data && fetcher.data.length > 0) { | |
setPhotos((prevPhotos: PhotoHash[]) => [...prevPhotos, ...fetcher.data]); | |
setPage((page: number) => page + 1); | |
setShouldFetch(true); | |
} | |
}, [fetcher.data]); | |
return ( | |
<div | |
ref={divHeight} | |
className="container mx-auto space-y-2 md:space-y-0 md:gap-2 md:grid md:grid-cols-2 py-4" | |
> | |
{photos.map((photo: PhotoHash) => { | |
return ( | |
<div className="w-full border-green-200 md:h-85" key={photo.pid}> | |
<img | |
className="mx-auto object-center object-cover h-full rounded hover:shadow-2xl" | |
src={photo.url} | |
alt={`photo-${photo.pid}`} | |
/> | |
</div> | |
); | |
})} | |
</div> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment