Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(react-virtual): add two way infinite scroll example #674

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/react/two-way-infinite-scroll/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
6 changes: 6 additions & 0 deletions examples/react/two-way-infinite-scroll/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example

To run this example:

- `npm install` or `yarn`
- `npm run start` or `yarn start`
13 changes: 13 additions & 0 deletions examples/react/two-way-infinite-scroll/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<script type="module" src="https://cdn.skypack.dev/twind/shim"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
25 changes: 25 additions & 0 deletions examples/react/two-way-infinite-scroll/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "tanstack-react-virtual-example-two-way-infinite-scroll",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview --port 3001",
"start": "vite"
},
"dependencies": {
"@tanstack/react-query": "^5.20.5",
"@tanstack/react-virtual": "^3.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@rollup/plugin-replace": "^5.0.2",
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react": "^4.2.1",
"typescript": "5.2.2",
"vite": "^5.1.3"
}
}
28 changes: 28 additions & 0 deletions examples/react/two-way-infinite-scroll/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
html {
font-family: sans-serif;
font-size: 14px;
}

body {
padding: 1rem;
}

.List {
border: 1px solid #e6e4dc;
max-width: 100%;
}

.ListItemEven,
.ListItemOdd {
display: flex;
align-items: center;
justify-content: center;
}

.ListItemEven {
background-color: #e6e4dc;
}

button {
border: 1px solid gray;
}
181 changes: 181 additions & 0 deletions examples/react/two-way-infinite-scroll/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
import {
InfiniteData,
QueryClient,
QueryClientProvider,
UseInfiniteQueryResult,
useInfiniteQuery,
} from '@tanstack/react-query'
import './index.css'
import { useWindowVirtualizer } from '@tanstack/react-virtual'

const queryClient = new QueryClient()

interface Page {
rows: string[]
nextOffset: number
}
async function fetchServerPage(
limit: number,
offset: number = 0,
): Promise<Page> {
const rows = new Array(limit)
.fill(0)
.map((e, i) => `Async loaded row #${i + offset * limit}`)

await new Promise((r) => setTimeout(r, 500))

return { rows, nextOffset: offset + 1 }
}

function App() {
const SCROLL_MARGIN = 200
const MAX_PAGE_LENGTH = 5
const ITEM_HEIGHT = 100
const SENTRY_ITEM_LENGTH = 2
const [dataPerPagePrefixSum, setDataPerPagePrefixSum] = useState<number[]>([
0,
])
const {
status,
data,
error,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
isFetchingPreviousPage,
fetchPreviousPage,
hasPreviousPage,
}: UseInfiniteQueryResult<
InfiniteData<Page, number>,
Error
> = useInfiniteQuery({
queryKey: ['projects'],
queryFn: async ({ pageParam }) => {
const res = await fetchServerPage(10, pageParam)
if (dataPerPagePrefixSum.length <= pageParam + 1) {
setDataPerPagePrefixSum((prev) => [
...prev,
(prev.slice(-1)[0] ?? 0) + res.rows.length,
])
}
return res
},
initialPageParam: 0,
getNextPageParam: (_, __, lastPageParam) => {
return lastPageParam + 1
},
getPreviousPageParam: (_, __, firstPageParam) => {
if (firstPageParam <= 0) {
return null
}
return firstPageParam - 1
},
maxPages: MAX_PAGE_LENGTH,
})

const allRows = data ? data.pages.flatMap((d) => d.rows) : []
const minPageParam = data?.pageParams[0] ?? 0
const maxPageParam = data?.pageParams.slice(-1)[0] ?? 0
const minPageDataLength = dataPerPagePrefixSum[minPageParam] ?? 0
const maxPageDataLength = dataPerPagePrefixSum[maxPageParam] ?? 0
const maxDataLength = dataPerPagePrefixSum.slice(-1)[0] ?? 0
const rowVirtualizer = useWindowVirtualizer({
count: maxDataLength,
estimateSize: () => ITEM_HEIGHT,
overscan: 5,
scrollMargin: SCROLL_MARGIN,
})
const virtualItems = rowVirtualizer.getVirtualItems()
React.useEffect(() => {
const firstItemKey = virtualItems[0]?.key as number | undefined
const lastItemKey = virtualItems.slice(-1)[0]?.key as number | undefined
if (
firstItemKey &&
firstItemKey < minPageDataLength + SENTRY_ITEM_LENGTH &&
!isFetchingPreviousPage &&
hasPreviousPage
) {
fetchPreviousPage()
}
if (
lastItemKey &&
lastItemKey > maxPageDataLength - SENTRY_ITEM_LENGTH &&
!isFetchingNextPage &&
hasNextPage
) {
fetchNextPage()
}
}, [
virtualItems,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
minPageDataLength,
])
return (
<div>
<p
style={{
height: SCROLL_MARGIN,
}}
>
This code uses React Query and React Virtual to implement an interactive
infinite scroll feature. Its main features include setting maxPages to
limit the maximum number of pages, and fetching data from the server for
the previous or next page when the user moves the scroll up or down.
This saves memory and render costs.
</p>

{status === 'pending' ? (
<p>Loading...</p>
) : status === 'error' ? (
<span>Error: {(error as Error).message}</span>
) : (
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: '100%',
display: 'flex',
flexDirection: 'column',
gap: '16px',
}}
>
{virtualItems.map((virtualRow) => {
const post = allRows[virtualRow.index - minPageDataLength]

return (
<div
key={virtualRow.index}
className={
virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven'
}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{post == null ? 'loading' : post}
</div>
)
})}
</div>
)}
</div>
)
}

ReactDOM.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>,
document.getElementById('root'),
)
13 changes: 13 additions & 0 deletions examples/react/two-way-infinite-scroll/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"composite": true,
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "./build/types",
"jsx": "react"
},
"files": ["src/main.tsx"],
"include": [
"src"
// "__tests__/**/*.test.*"
]
}
7 changes: 7 additions & 0 deletions examples/react/two-way-infinite-scroll/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
47 changes: 47 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.