Skip to content

Instantly share code, notes, and snippets.

@OlegLustenko
Created August 22, 2024 14:01
Show Gist options
  • Save OlegLustenko/3a2c6742e2bcbc81be8d9869cd9dba4d to your computer and use it in GitHub Desktop.
Save OlegLustenko/3a2c6742e2bcbc81be8d9869cd9dba4d to your computer and use it in GitHub Desktop.
RSC Virtualization and LoadMore
'use client'
import * as React from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
import { clsx } from 'clsx'
import { useRouter, useSearchParams } from 'next/navigation'
import { useRef, useTransition } from 'react'
import Link from 'next/link'
export function RowVirtualizerFixed({ rows = [] }: { rows: string[] }) {
const parentRef = React.useRef(null)
const totalRef = useRef<string[]>([])
const searchParams = useSearchParams()
if (searchParams.get('page') === '1') {
totalRef.current = rows
}
rows.forEach((row) => {
if (!totalRef.current.includes(row)) {
totalRef.current.push(row)
}
})
const allRows = totalRef.current
const hasNextPage = true
const rowVirtualizer = useVirtualizer({
count: hasNextPage ? allRows.length + 1 : allRows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100,
overscan: 5,
initialRect: {
width: 800,
height: 800,
},
})
return (
<div className="h-[800px]">
<div
ref={parentRef}
className="h-5/6 w-full max-w-full overflow-auto"
>
<div
className="relative w-full"
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const isLoaderRow = virtualRow.index > allRows.length - 1
const post = allRows[virtualRow.index]
return (
<div
key={virtualRow.index}
className={clsx(
'absolute inset-0 flex w-full items-center justify-center',
virtualRow.index % 2 ? 'bg-white' : 'bg-amber-200',
)}
style={{
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{isLoaderRow ? <LoadingMoreButton /> : post}
</div>
)
})}
</div>
</div>
</div>
)
}
const LoadingMoreButton = () => {
const searchParams = useSearchParams()
const router = useRouter()
const [isPending, startTransition] = useTransition()
const fetchNextPage = () => {
startTransition(() => {
router.push(`?page=${+searchParams.get('page')! + 1}`)
})
}
const isMaxPage = searchParams.get('page')! === '10'
if (isMaxPage) {
return (
<div className="focus:ring-blue-700-500 h-full w-full border border-amber-200 bg-blue-300 px-4 py-2 text-center text-2xl text-white focus:outline-none focus:ring-2 focus:ring-offset-2">
<p className="text-5xl">Max page reached</p>
<Link
href="?page=1"
scroll
>
go to page 1
</Link>
</div>
)
}
return (
<button
className="focus:ring-blue-700-500 h-full w-full border border-amber-200 bg-blue-300 px-4 py-2 text-center text-2xl text-white focus:outline-none focus:ring-2 focus:ring-offset-2"
onClick={fetchNextPage}
type="button"
disabled={isMaxPage}
>
{isPending ? <p className="text-5xl">Loading...</p> : 'Load more'}
</button>
)
}
import React from 'react'
import { RowVirtualizerFixed } from '@/app/workshops/rsc-virtualized/_list'
import { clsx } from 'clsx'
import { redirect } from 'next/navigation'
async function fetchServerPage(
limit: number,
offset: number = 0,
): Promise<string[]> {
const rows = new Array(limit)
.fill(0)
.map((e, i) => `Async loaded row #${i + offset * limit}`)
await new Promise((resolve) => {
setTimeout(resolve, 600)
})
return rows
}
const Page = async ({ searchParams }: { searchParams: { page: string } }) => {
if (!searchParams.page) {
redirect('?page=1')
}
const isMaxPage = +searchParams.page > 10
if (isMaxPage) {
redirect('?page=10')
}
const offset = searchParams.page === '1' ? 0 : +searchParams.page - 1
const rows = await fetchServerPage(10, offset)
return (
<div>
<h1 className={clsx('text-center text-2xl text-white')}>
Virtualization
{isMaxPage && (
<span className="text-center text-4xl text-fuchsia-600">
Max page reached
</span>
)}
</h1>
<RowVirtualizerFixed rows={rows} />
</div>
)
}
export default Page
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment