Skip to content
Merged
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
31 changes: 31 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
pull_request:
push:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
ci:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/ci-setup

- name: Format check
run: pnpm format:check

- name: Lint
run: pnpm lint

- name: Type check
run: pnpm check-types

- name: Build
run: pnpm build
55 changes: 55 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Tests

on:
pull_request:
push:
branches:
- main

jobs:
unit:
name: Unit
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/ci-setup

- run: pnpm test

e2e:
name: E2E
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/ci-setup

- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v5
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}

- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium

- name: Install Playwright OS deps
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps chromium

- name: Build example
run: pnpm --filter tanstack-start-example build

- name: Run Playwright tests
run: pnpm test:e2e

- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: playwright-report/
retention-days: 7
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,15 @@ node_modules

# Turborepo output
.turbo

# TanStack Router generated routes
**/routeTree.gen.ts

# Playwright
/playwright-report
/test-results
/blob-report
/playwright/.cache

# Vitest
coverage
7 changes: 7 additions & 0 deletions docs/vocs.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,12 @@ export default defineConfig(({ mode }) => ({
conditions: ['source', 'module', 'browser', 'default'],
}),
},
ssr: {
resolve: {
...(mode === 'development' && {
conditions: ['source', 'module', 'node', 'default'],
}),
},
},
},
}));
33 changes: 33 additions & 0 deletions examples/tanstack-start/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "tanstack-start-example",
"private": true,
"type": "module",
"version": "0.0.0",
"author": "wnhlee <2wheeh@gmail.com>",
"license": "MIT",
"packageManager": "pnpm@10.27.0",
"scripts": {
"dev": "vite dev",
"build": "vite build && tsc -p tsconfig.typecheck.json",
"preview": "vite preview",
"start": "node .output/server/index.mjs",
"check-types": "tsr generate && tsc -p tsconfig.typecheck.json"
},
"dependencies": {
"@tanstack/react-router": "^1.168.26",
"@tanstack/react-start": "^1.167.52",
"@tanstack/react-virtual": "^3.13.24",
"react": "catalog:",
"react-dom": "catalog:",
"react-virtual-masonry": "workspace:*"
},
"devDependencies": {
"@tanstack/router-cli": "^1.166.38",
"@types/node": "^22.10.5",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "^4.7.0",
"typescript": "catalog:",
"vite": "^7.0.0"
}
}
15 changes: 15 additions & 0 deletions examples/tanstack-start/src/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface Tile {
id: number;
height: number;
color: string;
}

const COLORS = ['#F47067', '#F4A261', '#E9C46A', '#2A9D8F', '#264653', '#8E7DBE'];

export function createTiles(count: number): Tile[] {
return Array.from({ length: count }, (_, i) => ({
id: i,
height: 80 + ((i * 37) % 320),
color: COLORS[i % COLORS.length]!,
}));
}
17 changes: 17 additions & 0 deletions examples/tanstack-start/src/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createRouter as createTanstackRouter } from '@tanstack/react-router';

import { routeTree } from './routeTree.gen';

export const getRouter = () => {
return createTanstackRouter({
routeTree,
scrollRestoration: true,
defaultPreload: 'intent',
});
};

declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof getRouter>;
}
}
69 changes: 69 additions & 0 deletions examples/tanstack-start/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/// <reference types="vite/client" />
import { HeadContent, Link, Outlet, Scripts, createRootRoute } from '@tanstack/react-router';

export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: 'TanStack Start | react-virtual-masonry SSR example' },
],
}),
component: RootComponent,
});

function RootComponent() {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body
style={{
margin: 0,
fontFamily: 'system-ui, sans-serif',
padding: 0,
}}
>
<nav
style={{
display: 'flex',
gap: 16,
padding: 16,
borderBottom: '1px solid #eee',
position: 'sticky',
top: 0,
background: 'white',
zIndex: 10,
}}
>
<Link
to="/"
activeProps={{ style: { fontWeight: 700 } }}
style={{ textDecoration: 'none', color: '#222' }}
>
client-only
</Link>
<Link
to="/ssr"
activeProps={{ style: { fontWeight: 700 } }}
style={{ textDecoration: 'none', color: '#222' }}
>
ssr (current behavior)
</Link>
<Link
to="/ssr-debug"
activeProps={{ style: { fontWeight: 700 } }}
style={{ textDecoration: 'none', color: '#222' }}
>
ssr-debug
</Link>
</nav>
<main style={{ padding: 16 }}>
<Outlet />
</main>
<Scripts />
</body>
</html>
);
}
50 changes: 50 additions & 0 deletions examples/tanstack-start/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createFileRoute } from '@tanstack/react-router';
import { Masonry } from 'react-virtual-masonry';

import { createTiles, type Tile } from '../data';

export const Route = createFileRoute('/')({
component: HomePage,
ssr: false,
});

const TILES = createTiles(200);

function HomePage() {
return (
<section data-testid="home">
<h1>Client-only Masonry</h1>
<p style={{ color: '#555' }}>
Baseline: this route disables SSR (<code>ssr: false</code>) and renders the Masonry purely
on the client. The HTML you receive from the server contains an empty placeholder.
</p>
<Masonry
data={TILES}
renderItem={renderTile}
columnsCountBreakPoints={{ 0: 1, 640: 2, 1024: 3, 1440: 4 }}
gutter={16}
estimateSize={(i) => TILES[i]!.height}
/>
</section>
);
}

function renderTile({ item, index }: { item: Tile; index: number }) {
return (
<div
data-testid="tile"
style={{
height: item.height,
background: item.color,
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 600,
borderRadius: 8,
}}
>
{index}
</div>
);
}
62 changes: 62 additions & 0 deletions examples/tanstack-start/src/routes/ssr-debug.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { createFileRoute } from '@tanstack/react-router';
import { useWindowVirtualizer } from '@tanstack/react-virtual';

export const Route = createFileRoute('/ssr-debug')({
component: SsrDebugPage,
});

const COUNT = 200;
const LANES = 3;

function SsrDebugPage() {
'use no memo';

const virtualizer = useWindowVirtualizer({
count: COUNT,
estimateSize: () => 200,
lanes: LANES,
gap: 16,
overscan: 3,
laneAssignmentMode: 'measured',
});

// Calling getVirtualItems() triggers the internal getMeasurements() memo chain,
// populating the public `measurementsCache` field as a side effect.
// We can't call getMeasurements() directly because it's marked `private` in TanStack Virtual.
const visibleItems = virtualizer.getVirtualItems();
const measurements = virtualizer.measurementsCache;

const snapshot = {
runtime: typeof window === 'undefined' ? 'server' : 'client',
count: COUNT,
lanes: LANES,
totalSize: virtualizer.getTotalSize(),
measurementsLength: measurements.length,
measurementsFirst: measurements[0] ?? null,
measurementsLast: measurements[measurements.length - 1] ?? null,
visibleItemsLength: visibleItems.length,
visibleItemsFirst: visibleItems[0] ?? null,
};

return (
<section data-testid="ssr-debug">
<h1>SSR debug snapshot</h1>
<p style={{ color: '#555' }}>
Inspect <code>useWindowVirtualizer</code> state at render time on both server and client.
Tests parse the JSON below to verify behavior across runtimes.
</p>
<pre
data-testid="snapshot"
style={{
padding: 16,
background: '#f6f6f6',
borderRadius: 8,
overflow: 'auto',
fontSize: 13,
}}
>
{JSON.stringify(snapshot, null, 2)}
</pre>
</section>
);
}
Loading
Loading