Skip to content

Instantly share code, notes, and snippets.

@ptenteromano
Last active March 28, 2024 10:42
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save ptenteromano/e42fe33622a26ba3fdd49b51109203c7 to your computer and use it in GitHub Desktop.
Save ptenteromano/e42fe33622a26ba3fdd49b51109203c7 to your computer and use it in GitHub Desktop.
Infinite Scroll with Remix Run
/*
* 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