Components
A bias-based infinite scroll wrapper and hook that manages pagination and masks child items for progressive "load more" lists.
Loading preview...
'use client'
import { MaskedList, useInfiniteList } from '@/components/ui/infinite-list'
import Image from 'next/image'
import { useEffect, useState, useTransition } from 'react'
import { Button } from '@/components/ui/button'
import { Card, CardFooter } from '@/components/ui/card'
type Pokemon = { name: string; url: string; image: string }
async function fetchPokemon(offset: number, limit: number): Promise<Pokemon[]> {
const res = await fetch(
`https://pokeapi.co/api/v2/pokemon?offset=${offset}&limit=${limit}`
).then((r) => r.json())
return Promise.all(
res.results.map(async (p: { url: string }) => {
const data = await fetch(p.url).then((r) => r.json())
return { name: data.name, url: p.url, image: data.sprites.front_default }
})
)
}
export default function InfiniteListDemo() {
const pageSize = 12
const [pokemon, setPokemon] = useState<Pokemon[]>([])
const [isPending, startTransition] = useTransition()
const list = useInfiniteList({ pageSize, initialItems: pokemon })
useEffect(() => {
fetchPokemon(0, pageSize * 2).then(setPokemon)
}, [])
const loadMore = () => {
list.nextPage()
startTransition(async () => {
const newPokemon = await fetchPokemon(list.offset, pageSize)
setPokemon((prev) => [...prev, ...newPokemon])
})
}
return (
<div className="relative h-[500px] w-full overflow-auto">
<div className="bg-background/90 border-border sticky top-4 right-8 z-10 ml-auto w-fit rounded-lg border px-3 py-2 font-mono text-xs">
DISPLAYED: {Math.min(list.displayLimit, pokemon.length)} | LOADED:{' '}
{pokemon.length}
</div>
<div className="flex flex-col gap-6 p-6">
<h2 className="text-2xl font-semibold">Pokemon Infinite List</h2>
<div className="mb-6 grid grid-cols-2 gap-3 md:grid-cols-3">
<MaskedList {...list}>
{pokemon.map((p) => (
<Card key={p.url} className="gap-0 overflow-hidden p-0">
<div className="bg-muted aspect-square">
<Image
src={p.image}
alt={p.name}
width={500}
height={500}
className="h-full w-full object-cover [image-rendering:pixelated]"
unoptimized
/>
</div>
<CardFooter className="p-4">
<h3 className="text-base font-semibold capitalize">
{p.name}
</h3>
</CardFooter>
</Card>
))}
</MaskedList>
</div>
<Button
onClick={loadMore}
disabled={isPending}
variant="outline"
className="self-center shadow-none transition-none"
>
{isPending ? 'Loading...' : 'Load More'}
</Button>
</div>
</div>
)
}