From 541328ea8aa9139cfa813bc1264237109859e5b9 Mon Sep 17 00:00:00 2001 From: bluejoyq Date: Sun, 18 Feb 2024 22:25:01 +0900 Subject: [PATCH 1/5] init --- .../react/two-way-infinite-scroll/.gitignore | 5 + .../react/two-way-infinite-scroll/README.md | 6 + .../react/two-way-infinite-scroll/index.html | 13 ++ .../two-way-infinite-scroll/package.json | 25 +++ .../two-way-infinite-scroll/src/index.css | 28 +++ .../two-way-infinite-scroll/src/main.tsx | 160 ++++++++++++++++++ .../two-way-infinite-scroll/tsconfig.json | 15 ++ .../two-way-infinite-scroll/vite.config.js | 7 + pnpm-lock.yaml | 47 +++++ 9 files changed, 306 insertions(+) create mode 100644 examples/react/two-way-infinite-scroll/.gitignore create mode 100644 examples/react/two-way-infinite-scroll/README.md create mode 100644 examples/react/two-way-infinite-scroll/index.html create mode 100644 examples/react/two-way-infinite-scroll/package.json create mode 100644 examples/react/two-way-infinite-scroll/src/index.css create mode 100644 examples/react/two-way-infinite-scroll/src/main.tsx create mode 100644 examples/react/two-way-infinite-scroll/tsconfig.json create mode 100644 examples/react/two-way-infinite-scroll/vite.config.js diff --git a/examples/react/two-way-infinite-scroll/.gitignore b/examples/react/two-way-infinite-scroll/.gitignore new file mode 100644 index 00000000..d451ff16 --- /dev/null +++ b/examples/react/two-way-infinite-scroll/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/examples/react/two-way-infinite-scroll/README.md b/examples/react/two-way-infinite-scroll/README.md new file mode 100644 index 00000000..b168d3c4 --- /dev/null +++ b/examples/react/two-way-infinite-scroll/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `yarn` +- `npm run start` or `yarn start` diff --git a/examples/react/two-way-infinite-scroll/index.html b/examples/react/two-way-infinite-scroll/index.html new file mode 100644 index 00000000..3fc40c93 --- /dev/null +++ b/examples/react/two-way-infinite-scroll/index.html @@ -0,0 +1,13 @@ + + + + + + Vite App + + + +
+ + + diff --git a/examples/react/two-way-infinite-scroll/package.json b/examples/react/two-way-infinite-scroll/package.json new file mode 100644 index 00000000..83d90681 --- /dev/null +++ b/examples/react/two-way-infinite-scroll/package.json @@ -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" + } +} \ No newline at end of file diff --git a/examples/react/two-way-infinite-scroll/src/index.css b/examples/react/two-way-infinite-scroll/src/index.css new file mode 100644 index 00000000..c46155fa --- /dev/null +++ b/examples/react/two-way-infinite-scroll/src/index.css @@ -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; +} diff --git a/examples/react/two-way-infinite-scroll/src/main.tsx b/examples/react/two-way-infinite-scroll/src/main.tsx new file mode 100644 index 00000000..a5507851 --- /dev/null +++ b/examples/react/two-way-infinite-scroll/src/main.tsx @@ -0,0 +1,160 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import { QueryClient, QueryClientProvider, useInfiniteQuery } from '@tanstack/react-query' +import './index.css' +import { useVirtualizer } from '@tanstack/react-virtual' + +const queryClient = new QueryClient() + +async function fetchServerPage( + limit: number, + offset: number = 0, +): Promise<{ rows: string[]; nextOffset: number }> { + 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 { + status, + data, + error, + isFetching, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + } = useInfiniteQuery( + + { + queryKey:['projects'], + queryFn:(ctx) => fetchServerPage(10, ctx.pageParam), + initialPageParam:0, + getNextPageParam: (_lastGroup, groups) => groups.length, + }, + ) + + const allRows = data ? data.pages.flatMap((d) => d.rows) : [] + + const parentRef = React.useRef(null) + + const rowVirtualizer = useVirtualizer({ + count: hasNextPage ? allRows.length + 1 : allRows.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 100, + overscan: 5, + }) + + React.useEffect(() => { + const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse() + + if (!lastItem) { + return + } + + if ( + lastItem.index >= allRows.length - 1 && + hasNextPage && + !isFetchingNextPage + ) { + fetchNextPage() + } + }, [ + hasNextPage, + fetchNextPage, + allRows.length, + isFetchingNextPage, + rowVirtualizer.getVirtualItems(), + ]) + + return ( +
+

+ This infinite scroll example uses React Query's useInfiniteScroll hook + to fetch infinite data from a posts endpoint and then a rowVirtualizer + is used along with a loader-row placed at the bottom of the list to + trigger the next page to load. +

+ +
+
+ + {status === 'pending' ? ( +

Loading...

+ ) : status === 'error' ? ( + Error: {(error as Error).message} + ) : ( +
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const isLoaderRow = virtualRow.index > allRows.length - 1 + const post = allRows[virtualRow.index] + + return ( +
+ {isLoaderRow + ? hasNextPage + ? 'Loading more...' + : 'Nothing more to load' + : post} +
+ ) + })} +
+
+ )} +
+ {isFetching && !isFetchingNextPage ? 'Background Updating...' : null} +
+
+
+ {process.env.NODE_ENV === 'development' ? ( +

+ Notice: You are currently running React in + development mode. Rendering performance will be slightly degraded + until this application is build for production. +

+ ) : null} +
+ ) +} + +ReactDOM.render( + + + + + , + document.getElementById('root'), +) diff --git a/examples/react/two-way-infinite-scroll/tsconfig.json b/examples/react/two-way-infinite-scroll/tsconfig.json new file mode 100644 index 00000000..5933f722 --- /dev/null +++ b/examples/react/two-way-infinite-scroll/tsconfig.json @@ -0,0 +1,15 @@ +{ + "composite": true, + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./build/types", + "jsx": "react" + }, + "files": [ + "src/main.tsx" + ], + "include": [ + "src" + // "__tests__/**/*.test.*" + ] +} \ No newline at end of file diff --git a/examples/react/two-way-infinite-scroll/vite.config.js b/examples/react/two-way-infinite-scroll/vite.config.js new file mode 100644 index 00000000..5a33944a --- /dev/null +++ b/examples/react/two-way-infinite-scroll/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e50e777..1ad7ee70 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,6 +275,40 @@ importers: specifier: ^5.1.3 version: 5.1.3(@types/node@18.19.17) + examples/react/two-way-infinite-scroll: + dependencies: + '@tanstack/react-query': + specifier: ^5.20.5 + version: 5.21.7(react@18.2.0) + '@tanstack/react-virtual': + specifier: ^3.1.1 + version: link:../../../packages/react-virtual + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + devDependencies: + '@rollup/plugin-replace': + specifier: ^5.0.2 + version: 5.0.5(rollup@4.12.0) + '@types/react': + specifier: ^18.2.56 + version: 18.2.56 + '@types/react-dom': + specifier: ^18.2.19 + version: 18.2.19 + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.2.1(vite@5.1.3) + typescript: + specifier: 5.2.2 + version: 5.2.2 + vite: + specifier: ^5.1.3 + version: 5.1.3(@types/node@18.19.17) + examples/react/variable: dependencies: '@tanstack/react-virtual': @@ -2070,6 +2104,19 @@ packages: resolution: {integrity: sha512-WbZztNmKq0t6QjdNmHzezbi/uifYo9j6e2GLJkodsYaYUlzMbAp91RDyeHkIZrm7EfO4wa6Sm5sxJZm5SPlh6w==} dev: false + /@tanstack/query-core@5.21.7: + resolution: {integrity: sha512-z0NSWFsM75esVmkxeuDWeyo9Wv4CZ/WsLMZgu1Zz164S6Oc/57NMia88dTu/d51wdVowMTAcDMQgRmiWmyPMxQ==} + dev: false + + /@tanstack/react-query@5.21.7(react@18.2.0): + resolution: {integrity: sha512-Op9nVL/L0Lg+aSPFSGbrymu6d3tzUoZ+FZ+rRIZpu5HkGasflqzhsXkL26Sa+MEwLyox7Q1erSFwNIRO3TYjXQ==} + peerDependencies: + react: ^18.0.0 + dependencies: + '@tanstack/query-core': 5.21.7 + react: 18.2.0 + dev: false + /@tanstack/react-table@8.10.7(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-bXhjA7xsTcsW8JPTTYlUg/FuBpn8MNjiEPhkNhIGCUR6iRQM2+WEco4OBpvDeVcR9SE+bmWLzdfiY7bCbCSVuA==} engines: {node: '>=12'} From d3a50ce7f9008a95da5d0a1ac30727a55577c2ff Mon Sep 17 00:00:00 2001 From: bluejoyq Date: Mon, 19 Feb 2024 23:26:08 +0900 Subject: [PATCH 2/5] feat: example draft --- .../two-way-infinite-scroll/src/main.tsx | 150 +++++++++--------- 1 file changed, 77 insertions(+), 73 deletions(-) diff --git a/examples/react/two-way-infinite-scroll/src/main.tsx b/examples/react/two-way-infinite-scroll/src/main.tsx index a5507851..3ae0c4be 100644 --- a/examples/react/two-way-infinite-scroll/src/main.tsx +++ b/examples/react/two-way-infinite-scroll/src/main.tsx @@ -1,15 +1,19 @@ -import React from 'react' +import React, { useCallback, useState } from 'react' import ReactDOM from 'react-dom' -import { QueryClient, QueryClientProvider, useInfiniteQuery } from '@tanstack/react-query' +import { InfiniteData, QueryClient, QueryClientProvider, UseInfiniteQueryResult, useInfiniteQuery } from '@tanstack/react-query' import './index.css' -import { useVirtualizer } from '@tanstack/react-virtual' +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<{ rows: string[]; nextOffset: number }> { +): Promise { const rows = new Array(limit) .fill(0) .map((e, i) => `Async loaded row #${i + offset * limit}`) @@ -20,93 +24,110 @@ async function fetchServerPage( } function App() { + const SCROLL_MARGIN = 200; + const MAX_PAGE_LENGTH = 5; + const ITEM_HEIGHT = 100; + const [dataPerPagePrefixSum, setDataPerPagePrefixSum] = useState([0]); const { status, data, error, - isFetching, isFetchingNextPage, fetchNextPage, hasNextPage, - } = useInfiniteQuery( - + isFetchingPreviousPage, + fetchPreviousPage, + hasPreviousPage, + } : UseInfiniteQueryResult, Error>= useInfiniteQuery( { queryKey:['projects'], - queryFn:(ctx) => fetchServerPage(10, ctx.pageParam), + 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: (_lastGroup, groups) => groups.length, + 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 parentRef = React.useRef(null) - - const rowVirtualizer = useVirtualizer({ - count: hasNextPage ? allRows.length + 1 : allRows.length, - getScrollElement: () => parentRef.current, - estimateSize: () => 100, + const allRows = data ? data.pages.flatMap((d) => d.rows) : []; + const minPageParam = data?.pageParams[0] ?? 0; + // 메모리 최적화를 위해 없어진 데이터의 갯수 + const minPageDataLength = (dataPerPagePrefixSum[minPageParam] ?? 0); + const maxDataLength = dataPerPagePrefixSum.slice(-1)[0] ?? 0; + const rowVirtualizer = useWindowVirtualizer({ + count: maxDataLength, + estimateSize: () => ITEM_HEIGHT, overscan: 5, + scrollMargin: SCROLL_MARGIN }) - - React.useEffect(() => { - const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse() - - if (!lastItem) { - return + const virtualItems = rowVirtualizer.getVirtualItems() + + const handleScroll = useCallback((): void => { + // 전체 문서의 높이 + const scrollHeight = document.documentElement.scrollHeight; + // 현재 스크롤 위치 + const scrollTop = document.documentElement.scrollTop; + // 뷰포트 높이 + const clientHeight = document.documentElement.clientHeight; + // 남은 스크롤 높이 계산 + const scrollRemaining = scrollHeight - scrollTop - clientHeight; + // 남은 스크롤이 아이템 2개 미만이면 다음 데이터 fetch + if (scrollRemaining < ITEM_HEIGHT * 2 && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); } - - if ( - lastItem.index >= allRows.length - 1 && - hasNextPage && - !isFetchingNextPage - ) { - fetchNextPage() + // 남은 스크롤이 아이템 2개 미만이면 이전 데이터 fetch + if (scrollTop-SCROLL_MARGIN < (minPageDataLength - 2) * ITEM_HEIGHT && hasPreviousPage && !isFetchingPreviousPage) { + fetchPreviousPage(); } - }, [ - hasNextPage, - fetchNextPage, - allRows.length, - isFetchingNextPage, - rowVirtualizer.getVirtualItems(), - ]) - + + }, [hasPreviousPage,isFetchingPreviousPage,hasNextPage,isFetchingNextPage,minPageDataLength]) + React.useEffect(() => { + document.addEventListener('scroll',handleScroll); + return ()=>{ + document.removeEventListener('scroll',handleScroll) + } + }, [handleScroll]) return (
-

+

This infinite scroll example uses React Query's useInfiniteScroll hook to fetch infinite data from a posts endpoint and then a rowVirtualizer is used along with a loader-row placed at the bottom of the list to trigger the next page to load.

-
-
- {status === 'pending' ? (

Loading...

) : status === 'error' ? ( Error: {(error as Error).message} ) : ( -
+
- {rowVirtualizer.getVirtualItems().map((virtualRow) => { - const isLoaderRow = virtualRow.index > allRows.length - 1 - const post = allRows[virtualRow.index] + {virtualItems.map((virtualRow) => { + const post = allRows[virtualRow.index - minPageDataLength] return (
- {isLoaderRow - ? hasNextPage - ? 'Loading more...' - : 'Nothing more to load' - : post} + {post == null ? 'loading' : post}
) })}
-
)} -
- {isFetching && !isFetchingNextPage ? 'Background Updating...' : null} -
-
-
- {process.env.NODE_ENV === 'development' ? ( -

- Notice: You are currently running React in - development mode. Rendering performance will be slightly degraded - until this application is build for production. -

- ) : null}
) } From 1710e98b1b43d5c0a8bd324026bcff49e7b6a5db Mon Sep 17 00:00:00 2001 From: bluejoyq Date: Tue, 20 Feb 2024 00:06:25 +0900 Subject: [PATCH 3/5] feat: alternate scroll event --- .../two-way-infinite-scroll/package.json | 2 +- .../two-way-infinite-scroll/src/main.tsx | 213 ++++++++++-------- .../two-way-infinite-scroll/tsconfig.json | 6 +- 3 files changed, 118 insertions(+), 103 deletions(-) diff --git a/examples/react/two-way-infinite-scroll/package.json b/examples/react/two-way-infinite-scroll/package.json index 83d90681..e95113f3 100644 --- a/examples/react/two-way-infinite-scroll/package.json +++ b/examples/react/two-way-infinite-scroll/package.json @@ -22,4 +22,4 @@ "typescript": "5.2.2", "vite": "^5.1.3" } -} \ No newline at end of file +} diff --git a/examples/react/two-way-infinite-scroll/src/main.tsx b/examples/react/two-way-infinite-scroll/src/main.tsx index 3ae0c4be..842d70b1 100644 --- a/examples/react/two-way-infinite-scroll/src/main.tsx +++ b/examples/react/two-way-infinite-scroll/src/main.tsx @@ -1,13 +1,19 @@ -import React, { useCallback, useState } from 'react' +import React, { useState } from 'react' import ReactDOM from 'react-dom' -import { InfiniteData, QueryClient, QueryClientProvider, UseInfiniteQueryResult, useInfiniteQuery } from '@tanstack/react-query' +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[], + rows: string[] nextOffset: number } async function fetchServerPage( @@ -24,10 +30,13 @@ async function fetchServerPage( } function App() { - const SCROLL_MARGIN = 200; - const MAX_PAGE_LENGTH = 5; - const ITEM_HEIGHT = 100; - const [dataPerPagePrefixSum, setDataPerPagePrefixSum] = useState([0]); + const SCROLL_MARGIN = 200 + const MAX_PAGE_LENGTH = 5 + const ITEM_HEIGHT = 100 + const SENTRY_ITEM_LENGTH = 2 + const [dataPerPagePrefixSum, setDataPerPagePrefixSum] = useState([ + 0, + ]) const { status, data, @@ -38,77 +47,86 @@ function App() { isFetchingPreviousPage, fetchPreviousPage, hasPreviousPage, - } : UseInfiniteQueryResult, 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, + }: UseInfiniteQueryResult< + InfiniteData, + 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 minPageDataLength = (dataPerPagePrefixSum[minPageParam] ?? 0); - const maxDataLength = dataPerPagePrefixSum.slice(-1)[0] ?? 0; + 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 + scrollMargin: SCROLL_MARGIN, }) const virtualItems = rowVirtualizer.getVirtualItems() - - const handleScroll = useCallback((): void => { - // 전체 문서의 높이 - const scrollHeight = document.documentElement.scrollHeight; - // 현재 스크롤 위치 - const scrollTop = document.documentElement.scrollTop; - // 뷰포트 높이 - const clientHeight = document.documentElement.clientHeight; - // 남은 스크롤 높이 계산 - const scrollRemaining = scrollHeight - scrollTop - clientHeight; - // 남은 스크롤이 아이템 2개 미만이면 다음 데이터 fetch - if (scrollRemaining < ITEM_HEIGHT * 2 && hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } - // 남은 스크롤이 아이템 2개 미만이면 이전 데이터 fetch - if (scrollTop-SCROLL_MARGIN < (minPageDataLength - 2) * ITEM_HEIGHT && hasPreviousPage && !isFetchingPreviousPage) { - fetchPreviousPage(); - } - - }, [hasPreviousPage,isFetchingPreviousPage,hasNextPage,isFetchingNextPage,minPageDataLength]) React.useEffect(() => { - document.addEventListener('scroll',handleScroll); - return ()=>{ - document.removeEventListener('scroll',handleScroll) + 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() } - }, [handleScroll]) + if ( + lastItemKey && + lastItemKey > maxPageDataLength - SENTRY_ITEM_LENGTH && + !isFetchingNextPage && + hasNextPage + ) { + fetchNextPage() + } + }, [ + virtualItems, + hasNextPage, + hasPreviousPage, + isFetchingNextPage, + isFetchingPreviousPage, + minPageDataLength, + ]) return (
-

- This infinite scroll example uses React Query's useInfiniteScroll hook - to fetch infinite data from a posts endpoint and then a rowVirtualizer - is used along with a loader-row placed at the bottom of the list to - trigger the next page to load. +

+ 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.

{status === 'pending' ? ( @@ -116,39 +134,38 @@ function App() { ) : status === 'error' ? ( Error: {(error as Error).message} ) : ( +
+ {virtualItems.map((virtualRow) => { + const post = allRows[virtualRow.index - minPageDataLength] -
- {virtualItems.map((virtualRow) => { - const post = allRows[virtualRow.index - minPageDataLength] - - return ( -
- {post == null ? 'loading' : post} -
- ) - })} -
+ return ( +
+ {post == null ? 'loading' : post} +
+ ) + })} +
)}
) diff --git a/examples/react/two-way-infinite-scroll/tsconfig.json b/examples/react/two-way-infinite-scroll/tsconfig.json index 5933f722..d6bbf775 100644 --- a/examples/react/two-way-infinite-scroll/tsconfig.json +++ b/examples/react/two-way-infinite-scroll/tsconfig.json @@ -5,11 +5,9 @@ "outDir": "./build/types", "jsx": "react" }, - "files": [ - "src/main.tsx" - ], + "files": ["src/main.tsx"], "include": [ "src" // "__tests__/**/*.test.*" ] -} \ No newline at end of file +} From 0c2041be64255d3c9c3b1c67b8cb6465fc3b3b93 Mon Sep 17 00:00:00 2001 From: Lachlan Collins <1667261+lachlancollins@users.noreply.github.com> Date: Sat, 8 Mar 2025 11:59:33 +1100 Subject: [PATCH 4/5] Update example config --- .../two-way-infinite-scroll/package.json | 2 +- .../two-way-infinite-scroll/tsconfig.json | 28 +++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/examples/react/two-way-infinite-scroll/package.json b/examples/react/two-way-infinite-scroll/package.json index 997b4ec4..07e7614d 100644 --- a/examples/react/two-way-infinite-scroll/package.json +++ b/examples/react/two-way-infinite-scroll/package.json @@ -1,7 +1,7 @@ { "name": "tanstack-react-virtual-example-two-way-infinite-scroll", - "version": "0.0.0", "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "vite build", diff --git a/examples/react/two-way-infinite-scroll/tsconfig.json b/examples/react/two-way-infinite-scroll/tsconfig.json index d6bbf775..87318025 100644 --- a/examples/react/two-way-infinite-scroll/tsconfig.json +++ b/examples/react/two-way-infinite-scroll/tsconfig.json @@ -1,13 +1,25 @@ { "composite": true, - "extends": "../../../tsconfig.json", "compilerOptions": { - "outDir": "./build/types", - "jsx": "react" + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true }, - "files": ["src/main.tsx"], - "include": [ - "src" - // "__tests__/**/*.test.*" - ] + "include": ["src"] } From 6771faacf48c0c5ff02252b9229f9828f040a8b0 Mon Sep 17 00:00:00 2001 From: Lachlan Collins <1667261+lachlancollins@users.noreply.github.com> Date: Sat, 8 Mar 2025 12:01:30 +1100 Subject: [PATCH 5/5] Fix sherif issue --- .../two-way-infinite-scroll/package.json | 2 +- pnpm-lock.yaml | 46 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/examples/react/two-way-infinite-scroll/package.json b/examples/react/two-way-infinite-scroll/package.json index 07e7614d..f9b9e610 100644 --- a/examples/react/two-way-infinite-scroll/package.json +++ b/examples/react/two-way-infinite-scroll/package.json @@ -9,7 +9,7 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-query": "^5.66.1", + "@tanstack/react-query": "^5.66.11", "@tanstack/react-virtual": "^3.13.2", "react": "^18.3.1", "react-dom": "^18.3.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75f7b324..03213ab3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -832,7 +832,7 @@ importers: examples/react/two-way-infinite-scroll: dependencies: '@tanstack/react-query': - specifier: ^5.66.1 + specifier: ^5.66.11 version: 5.66.11(react@18.3.1) '@tanstack/react-virtual': specifier: ^3.13.2 @@ -7886,12 +7886,12 @@ snapshots: '@vitejs/plugin-basic-ssl': 1.1.0(vite@5.4.14(@types/node@22.13.7)(less@4.2.0)(sass@1.71.1)(terser@5.29.1)) ansi-colors: 4.1.3 autoprefixer: 10.4.18(postcss@8.4.35) - babel-loader: 9.1.3(@babel/core@7.24.0)(webpack@5.94.0(esbuild@0.20.1)) + babel-loader: 9.1.3(@babel/core@7.24.0)(webpack@5.94.0) babel-plugin-istanbul: 6.1.1 browserslist: 4.24.4 - copy-webpack-plugin: 11.0.0(webpack@5.94.0(esbuild@0.20.1)) + copy-webpack-plugin: 11.0.0(webpack@5.94.0) critters: 0.0.22 - css-loader: 6.10.0(webpack@5.94.0(esbuild@0.20.1)) + css-loader: 6.10.0(webpack@5.94.0) esbuild-wasm: 0.20.1 fast-glob: 3.3.2 http-proxy-middleware: 2.0.7(@types/express@4.17.21) @@ -7900,11 +7900,11 @@ snapshots: jsonc-parser: 3.2.1 karma-source-map-support: 1.4.0 less: 4.2.0 - less-loader: 11.1.0(less@4.2.0)(webpack@5.94.0(esbuild@0.20.1)) - license-webpack-plugin: 4.0.2(webpack@5.94.0(esbuild@0.20.1)) + less-loader: 11.1.0(less@4.2.0)(webpack@5.94.0) + license-webpack-plugin: 4.0.2(webpack@5.94.0) loader-utils: 3.2.1 magic-string: 0.30.8 - mini-css-extract-plugin: 2.8.1(webpack@5.94.0(esbuild@0.20.1)) + mini-css-extract-plugin: 2.8.1(webpack@5.94.0) mrmime: 2.0.0 open: 8.4.2 ora: 5.4.1 @@ -7912,13 +7912,13 @@ snapshots: picomatch: 4.0.1 piscina: 4.4.0 postcss: 8.4.35 - postcss-loader: 8.1.1(postcss@8.4.35)(typescript@5.2.2)(webpack@5.94.0(esbuild@0.20.1)) + postcss-loader: 8.1.1(postcss@8.4.35)(typescript@5.2.2)(webpack@5.94.0) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.71.1 - sass-loader: 14.1.1(sass@1.71.1)(webpack@5.94.0(esbuild@0.20.1)) + sass-loader: 14.1.1(sass@1.71.1)(webpack@5.94.0) semver: 7.6.0 - source-map-loader: 5.0.0(webpack@5.94.0(esbuild@0.20.1)) + source-map-loader: 5.0.0(webpack@5.94.0) source-map-support: 0.5.21 terser: 5.29.1 tree-kill: 1.2.2 @@ -7928,10 +7928,10 @@ snapshots: vite: 5.4.14(@types/node@22.13.7)(less@4.2.0)(sass@1.71.1)(terser@5.29.1) watchpack: 2.4.0 webpack: 5.94.0(esbuild@0.20.1) - webpack-dev-middleware: 6.1.2(webpack@5.94.0(esbuild@0.20.1)) + webpack-dev-middleware: 6.1.2(webpack@5.94.0) webpack-dev-server: 4.15.1(webpack@5.94.0(esbuild@0.20.1)) webpack-merge: 5.10.0 - webpack-subresource-integrity: 5.1.0(webpack@5.94.0(esbuild@0.20.1)) + webpack-subresource-integrity: 5.1.0(webpack@5.94.0) optionalDependencies: esbuild: 0.20.1 ng-packagr: 17.3.0(@angular/compiler-cli@17.3.12(@angular/compiler@17.3.12(@angular/core@17.3.12(rxjs@7.8.2)(zone.js@0.15.0)))(typescript@5.2.2))(tslib@2.8.1)(typescript@5.2.2) @@ -11025,7 +11025,7 @@ snapshots: axobject-query@4.1.0: {} - babel-loader@9.1.3(@babel/core@7.24.0)(webpack@5.94.0(esbuild@0.20.1)): + babel-loader@9.1.3(@babel/core@7.24.0)(webpack@5.94.0): dependencies: '@babel/core': 7.24.0 find-cache-dir: 4.0.0 @@ -11382,7 +11382,7 @@ snapshots: dependencies: is-what: 3.14.1 - copy-webpack-plugin@11.0.0(webpack@5.94.0(esbuild@0.20.1)): + copy-webpack-plugin@11.0.0(webpack@5.94.0): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -11423,7 +11423,7 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-loader@6.10.0(webpack@5.94.0(esbuild@0.20.1)): + css-loader@6.10.0(webpack@5.94.0): dependencies: icss-utils: 5.1.0(postcss@8.5.3) postcss: 8.5.3 @@ -12753,7 +12753,7 @@ snapshots: picocolors: 1.1.1 shell-quote: 1.8.2 - less-loader@11.1.0(less@4.2.0)(webpack@5.94.0(esbuild@0.20.1)): + less-loader@11.1.0(less@4.2.0)(webpack@5.94.0): dependencies: klona: 2.0.6 less: 4.2.0 @@ -12792,7 +12792,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - license-webpack-plugin@4.0.2(webpack@5.94.0(esbuild@0.20.1)): + license-webpack-plugin@4.0.2(webpack@5.94.0): dependencies: webpack-sources: 3.2.3 optionalDependencies: @@ -12990,7 +12990,7 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.8.1(webpack@5.94.0(esbuild@0.20.1)): + mini-css-extract-plugin@2.8.1(webpack@5.94.0): dependencies: schema-utils: 4.3.0 tapable: 2.2.1 @@ -13541,7 +13541,7 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 - postcss-loader@8.1.1(postcss@8.4.35)(typescript@5.2.2)(webpack@5.94.0(esbuild@0.20.1)): + postcss-loader@8.1.1(postcss@8.4.35)(typescript@5.2.2)(webpack@5.94.0): dependencies: cosmiconfig: 9.0.0(typescript@5.2.2) jiti: 1.21.7 @@ -13898,7 +13898,7 @@ snapshots: safer-buffer@2.1.2: {} - sass-loader@14.1.1(sass@1.71.1)(webpack@5.94.0(esbuild@0.20.1)): + sass-loader@14.1.1(sass@1.71.1)(webpack@5.94.0): dependencies: neo-async: 2.6.2 optionalDependencies: @@ -14169,7 +14169,7 @@ snapshots: source-map-js@1.2.1: {} - source-map-loader@5.0.0(webpack@5.94.0(esbuild@0.20.1)): + source-map-loader@5.0.0(webpack@5.94.0): dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 @@ -14788,7 +14788,7 @@ snapshots: schema-utils: 4.3.0 webpack: 5.94.0(esbuild@0.20.1) - webpack-dev-middleware@6.1.2(webpack@5.94.0(esbuild@0.20.1)): + webpack-dev-middleware@6.1.2(webpack@5.94.0): dependencies: colorette: 2.0.20 memfs: 3.5.3 @@ -14846,7 +14846,7 @@ snapshots: webpack-sources@3.2.3: {} - webpack-subresource-integrity@5.1.0(webpack@5.94.0(esbuild@0.20.1)): + webpack-subresource-integrity@5.1.0(webpack@5.94.0): dependencies: typed-assert: 1.0.9 webpack: 5.94.0(esbuild@0.20.1)