Created
September 7, 2022 02:05
-
-
Save jacob-ebey/970d8082df1660bace3121c94980afd3 to your computer and use it in GitHub Desktop.
Remix Deferred Infinite Scrolling
This file contains 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
import * as React from "react"; | |
import { defer, type LoaderArgs } from "@remix-run/node"; | |
import { | |
Await, | |
Link, | |
Outlet, | |
useLoaderData, | |
useLocation, | |
useNavigate, | |
useTransition, | |
useSearchParams, | |
type ShouldReloadFunction, | |
} from "@remix-run/react"; | |
import useInfiniteScroll from "react-infinite-scroll-hook"; | |
import { pokemonClient } from "~/pokemon"; | |
import { | |
Panel, | |
PanelHeader, | |
PanelItem, | |
PanelItemLink, | |
PanelMain, | |
} from "~/components/panel"; | |
import iconsHref from "~/icons.svg"; | |
const PER_PAGE = 30; | |
export function loader({ request }: LoaderArgs) { | |
const url = new URL(request.url); | |
let page = Number(url.searchParams.get("page")); | |
page = Number.isSafeInteger(page) ? page : 0; | |
const skip = page * PER_PAGE; | |
return defer({ | |
page, | |
pokemon: pokemonClient.listPokemons(skip, PER_PAGE).then((result) => { | |
return result.results.map((pokemon) => { | |
return { | |
id: pokemon.url.split("/").slice(-2, -1)[0], | |
name: pokemon.name, | |
}; | |
}); | |
}), | |
}); | |
} | |
export const unstable_shouldReload: ShouldReloadFunction = ({ | |
url, | |
prevUrl, | |
}) => { | |
return ( | |
!!url.searchParams.get("page") && | |
url.searchParams.get("page") !== prevUrl.searchParams.get("page") | |
); | |
}; | |
export default function PokemonList() { | |
const loaderData = useLoaderData<typeof loader>(); | |
const location = useLocation(); | |
const navigate = useNavigate(); | |
const transition = useTransition(); | |
const [searchParams] = useSearchParams(); | |
const forceShow = location.pathname === "/dashboard/pokemon"; | |
const panelOpen = searchParams.get("open") === "list"; | |
const currentPage = searchParams.get("page"); | |
const nextPage = new URLSearchParams(transition.location?.search).get("page"); | |
const loadingNextPage = !!nextPage && nextPage !== currentPage; | |
const [loadedPages, setLoadedPages] = React.useState< | |
[number, Awaited<typeof loaderData["pokemon"]>][] | |
>([]); | |
React.useEffect(() => { | |
let canceled = false; | |
loaderData.pokemon.then((pokemons) => { | |
if (canceled) return; | |
setLoadedPages((loadedPages) => [ | |
...loadedPages.filter((page) => page[0] !== loaderData.page), | |
[loaderData.page, pokemons], | |
]); | |
}); | |
return () => { | |
canceled = true; | |
}; | |
}, [loaderData.pokemon, loaderData.page]); | |
const pokemonToRender = React.useMemo(() => { | |
if (!loadedPages.length) return loaderData.pokemon; | |
const pages = loadedPages.sort(([a], [b]) => a - b); | |
return pages.flatMap(([, pokemons]) => pokemons); | |
}, [loaderData.pokemon, loadedPages]); | |
const [sentryRef] = useInfiniteScroll({ | |
loading: transition.state === "loading", | |
hasNextPage: true, | |
onLoadMore: () => { | |
console.log("LOADING MORE"); | |
navigate(`?page=${loaderData.page + 1}`, { replace: true }); | |
}, | |
}); | |
return ( | |
<> | |
<Panel size="sm" open={panelOpen} force={forceShow}> | |
<PanelHeader> | |
<Link to="?open=menu" className="icon mr xl:hidden"> | |
<svg height={20} width={20}> | |
<use href={iconsHref + "#menu"} /> | |
</svg> | |
</Link> | |
Pokemon | |
</PanelHeader> | |
<PanelMain> | |
<React.Suspense> | |
<Await resolve={pokemonToRender}> | |
{(pokemons) => { | |
return ( | |
<> | |
{loaderData.page > 0 && ( | |
<PanelItemLink | |
replace | |
to={`?page=${loaderData.page - 1}#${pokemons[0]?.id}`} | |
> | |
Load Previous | |
</PanelItemLink> | |
)} | |
{pokemons.map((pokemon, index) => ( | |
<PanelItemLink | |
key={pokemon.id} | |
to={pokemon.id} | |
id={pokemon.id} | |
> | |
<img | |
ref={ | |
index === pokemons.length - 1 | |
? sentryRef | |
: undefined | |
} | |
height={24} | |
width={24} | |
className="mr" | |
src={`/pokemon-sprites/${pokemon.id}.svg`} | |
alt="" | |
/> | |
<span className="capitalize flex flex-center"> | |
{pokemon.name} | |
</span> | |
</PanelItemLink> | |
))} | |
</> | |
); | |
}} | |
</Await> | |
</React.Suspense> | |
{loadingNextPage && ( | |
<PanelItem> | |
<span className="capitalize flex flex-center">Loading...</span> | |
</PanelItem> | |
)} | |
</PanelMain> | |
</Panel> | |
<Outlet /> | |
</> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment