diff --git a/examples/react/lanes/.gitignore b/examples/react/lanes/.gitignore new file mode 100644 index 00000000..d451ff16 --- /dev/null +++ b/examples/react/lanes/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/examples/react/lanes/README.md b/examples/react/lanes/README.md new file mode 100644 index 00000000..b168d3c4 --- /dev/null +++ b/examples/react/lanes/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/lanes/index.html b/examples/react/lanes/index.html new file mode 100644 index 00000000..b3016791 --- /dev/null +++ b/examples/react/lanes/index.html @@ -0,0 +1,12 @@ + + + + + + React Virtual Lanes Example + + +
+ + + diff --git a/examples/react/lanes/package.json b/examples/react/lanes/package.json new file mode 100644 index 00000000..64032174 --- /dev/null +++ b/examples/react/lanes/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-react-virtual-example-lanes", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview --port 3001", + "start": "vite" + }, + "dependencies": { + "@tanstack/react-pacer": "^0.2.0", + "@tanstack/react-virtual": "^3.13.12", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.5.2", + "vite": "^5.4.19" + } +} diff --git a/examples/react/lanes/src/index.css b/examples/react/lanes/src/index.css new file mode 100644 index 00000000..c46155fa --- /dev/null +++ b/examples/react/lanes/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/lanes/src/main.tsx b/examples/react/lanes/src/main.tsx new file mode 100644 index 00000000..3fa5d354 --- /dev/null +++ b/examples/react/lanes/src/main.tsx @@ -0,0 +1,354 @@ +import * as React from 'react' +import * as ReactDOM from 'react-dom/client' + +import './index.css' + +import { useVirtualizer } from '@tanstack/react-virtual' +import { debounce } from '@tanstack/react-pacer' + +function App() { + return ( +
+

+ Lanes are useful when you are trying to draw a grid of + items, where each row is split into multiple columns. +

+
+
+ +

Lanes

+ +
+
+

Lanes Gaps

+ +
+
+

Resizable Container Lanes

+ +
+
+
+
+ {process.env.NODE_ENV === 'development' ? ( +

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

+ ) : null} +
+ ) +} + +function LanesVirtualizer() { + const [numLanes, setNumLanes] = React.useState(4) + const parentRef = React.useRef(null) + + const rowVirtualizer = useVirtualizer({ + count: 10000, + getScrollElement: () => parentRef.current, + estimateSize: () => 35, + overscan: 5, + lanes: numLanes, + }) + + return ( + <> +
+ + { + setNumLanes(Number(e.target.value)) + rowVirtualizer.measure() + }} + /> +
+
+
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => ( +
+ Cell {virtualRow.index} +
+ ))} +
+
+ + ) +} + +function GapVirtualizer() { + const parentRef = React.useRef(null) + const [numLanes, setNumLanes] = React.useState(4) + const [rowGap, setRowGap] = React.useState(10) + const [columnGap, setColumnGap] = React.useState(10) + + const rowVirtualizer = useVirtualizer({ + count: 10000, + getScrollElement: () => parentRef.current, + estimateSize: () => 35, + overscan: 5, + lanes: numLanes, + gap: rowGap, + }) + + return ( + <> +
+ + { + setNumLanes(Number(e.target.value)) + rowVirtualizer.measure() + }} + /> + + { + setRowGap(Number(e.target.value)) + rowVirtualizer.measure() + }} + /> + + { + setColumnGap(Number(e.target.value)) + rowVirtualizer.measure() + }} + /> +
+
+ +
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + return ( +
+ Cell {virtualRow.index} +
+ ) + })} +
+
+ + ) +} + +const CELL_WIDTH = 100 +function ResizeVirtualizer() { + const parentRef = React.useRef(null) + const [numLanes, setNumLanes] = React.useState(4) + const [rowGap, setRowGap] = React.useState(10) + const [columnGap, setColumnGap] = React.useState(10) + + const rowVirtualizer = useVirtualizer({ + count: 10000, + getScrollElement: () => parentRef.current, + estimateSize: () => 35, + overscan: 5, + lanes: numLanes, + gap: rowGap, + }) + + React.useEffect(() => { + if (!parentRef.current) return + // debounce not necessary + const debouncedOnResize = debounce( + (entries: Array) => { + const rect = entries.at(0)?.contentRect + if (!rect) return + const { width } = rect + setNumLanes(Math.floor(width / CELL_WIDTH)) + rowVirtualizer.measure() + }, + { + wait: 50, + }, + ) + const resizeObserver = new ResizeObserver((entries) => { + debouncedOnResize(entries) + }) + resizeObserver.observe(parentRef.current) + return () => { + resizeObserver.disconnect() + } + }, [rowVirtualizer]) + + return ( + <> +
+ + + + { + setRowGap(Number(e.target.value)) + rowVirtualizer.measure() + }} + /> + + { + setColumnGap(Number(e.target.value)) + rowVirtualizer.measure() + }} + /> +
+
+ +
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + return ( +
+ Cell {virtualRow.index} +
+ ) + })} +
+
+ + ) +} + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/examples/react/lanes/tsconfig.json b/examples/react/lanes/tsconfig.json new file mode 100644 index 00000000..87318025 --- /dev/null +++ b/examples/react/lanes/tsconfig.json @@ -0,0 +1,25 @@ +{ + "composite": true, + "compilerOptions": { + "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 + }, + "include": ["src"] +} diff --git a/examples/react/lanes/vite.config.js b/examples/react/lanes/vite.config.js new file mode 100644 index 00000000..5a33944a --- /dev/null +++ b/examples/react/lanes/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 90d30066..1f4d461f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -695,6 +695,34 @@ importers: specifier: ^5.4.19 version: 5.4.19(@types/node@24.5.2)(less@4.4.0)(sass@1.89.2)(terser@5.43.1) + examples/react/lanes: + dependencies: + '@tanstack/react-pacer': + specifier: ^0.2.0 + version: 0.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-virtual': + specifier: ^3.13.12 + version: link:../../../packages/react-virtual + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.23 + version: 18.3.23 + '@types/react-dom': + specifier: ^18.3.7 + version: 18.3.7(@types/react@18.3.23) + '@vitejs/plugin-react': + specifier: ^4.5.2 + version: 4.7.0(vite@5.4.19(@types/node@24.5.2)(less@4.4.0)(sass@1.89.2)(terser@5.43.1)) + vite: + specifier: ^5.4.19 + version: 5.4.19(@types/node@24.5.2)(less@4.4.0)(sass@1.89.2)(terser@5.43.1) + examples/react/padding: dependencies: '@tanstack/react-virtual': @@ -3806,6 +3834,10 @@ packages: resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} engines: {node: '>=12'} + '@tanstack/pacer@0.2.0': + resolution: {integrity: sha512-fUJs3NpSwtAL/tfq8kuYdgvm9HbbJvHsOG6aHY2dFDfff0NBFNwjvyGreWZZRPs2zgoIbr4nOk+rRV7aQgmf+A==} + engines: {node: '>=18'} + '@tanstack/publish-config@0.2.1': resolution: {integrity: sha512-URVXmXwlZXL75AFyvyOORef1tv2f16dEaFntwLYnBHoKLQMxyWYRzQrnXooxO1xf+GidJuDSZSC6Rc9UX1aK7g==} engines: {node: '>=18'} @@ -3819,6 +3851,13 @@ packages: '@tanstack/query-devtools@5.80.0': resolution: {integrity: sha512-D6gH4asyjaoXrCOt5vG5Og/YSj0D/TxwNQgtLJIgWbhbWCC/emu2E92EFoVHh4ppVWg1qT2gKHvKyQBEFZhCuA==} + '@tanstack/react-pacer@0.2.0': + resolution: {integrity: sha512-KU5GtjkKSeNdYCilen5Dc+Pu/6BPQbsQshKrUUjrg7URyJIiGBCz6ZZFre1QjDz/aeUeqUJWMWSm+2Dsh64v+w==} + engines: {node: '>=18'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + '@tanstack/react-query@5.84.0': resolution: {integrity: sha512-iPycFGLq5lltDE16Jf13Nx7SOvtfoopfOH/+Ahbdd+z4QqOfYu/SOkY86AVYVcKjneuqPxTm8e85lSGhwe0cog==} peerDependencies: @@ -11014,6 +11053,8 @@ snapshots: dependencies: remove-accents: 0.5.0 + '@tanstack/pacer@0.2.0': {} + '@tanstack/publish-config@0.2.1': dependencies: '@commitlint/parse': 19.8.1 @@ -11029,6 +11070,12 @@ snapshots: '@tanstack/query-devtools@5.80.0': {} + '@tanstack/react-pacer@0.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/pacer': 0.2.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@tanstack/react-query@5.84.0(react@18.3.1)': dependencies: '@tanstack/query-core': 5.83.1