diff --git a/src/List.tsx b/src/List.tsx index 046cae2..46b199c 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -146,7 +146,16 @@ export function RawList(props: ListProps, ref: React.Ref) { () => Object.values(heights.maps).reduce((total, curr) => total + curr, 0), [heights.id, heights.maps], ); + // Defer virtual rendering until after first commit so SSR emits stable, hydration-safe + // markup (no virtual scrollbar / Filler wrap that depends on client-side measurement). + // `useLayoutEffect` here only fires on the client, so `mounted` stays `false` on the + // server and on the first client render that hydrates the SSR output. + const [mounted, setMounted] = useState(false); + useLayoutEffect(() => { + setMounted(true); + }, []); const inVirtual = + mounted && useVirtual && data && (Math.max(itemHeight * data.length, containerHeight) > height || !!scrollWidth); @@ -587,7 +596,7 @@ export function RawList(props: ListProps, ref: React.Ref) { if (height) { componentStyle = { [fullHeight ? 'height' : 'maxHeight']: height, ...ScrollStyle }; - if (useVirtual) { + if (inVirtual) { componentStyle.overflowY = 'hidden'; if (scrollWidth) { diff --git a/tests/ssr.test.tsx b/tests/ssr.test.tsx new file mode 100644 index 0000000..54305da --- /dev/null +++ b/tests/ssr.test.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { renderToString } from 'react-dom/server'; +import List from '../src'; + +const ITEM_HEIGHT = 20; + +function genData(count: number) { + return new Array(count).fill(null).map((_, index) => ({ id: String(index) })); +} + +describe('List.ssr', () => { + function ssr(extra?: Record) { + return renderToString( + + data={genData(100)} + height={100} + itemHeight={ITEM_HEIGHT} + itemKey="id" + {...extra} + > + {(item) =>
{item.id}
} + , + ); + } + + it('initial SSR render should not enter virtual mode (no scrollbar / Filler wrap)', () => { + const html = ssr(); + expect(html).not.toContain('rc-virtual-list-scrollbar'); + // No Filler outer wrap with translateY (only emitted when inVirtual is true) + expect(html).not.toContain('translateY'); + }); + + it('initial SSR render should use native overflow:auto on holder', () => { + const html = ssr(); + // overflow-y stays "auto" before mount measures the container + expect(html).toContain('overflow-y:auto'); + expect(html).not.toContain('overflow-y:hidden'); + }); + + it('initial SSR render should not render horizontal scrollbar even when scrollWidth is set', () => { + const html = ssr({ scrollWidth: 200 }); + expect(html).not.toContain('rc-virtual-list-scrollbar'); + expect(html).not.toContain('overflow-x:hidden'); + }); + + it('initial SSR render emits all items so the page is readable without JS', () => { + const html = ssr(); + // First and last item should both appear in SSR output + expect(html).toContain('>0'); + expect(html).toContain('>99'); + }); +});