import { useEffect, useMemo, useRef, useState } from 'react'
import { faker } from '@faker-js/faker'

type Person = {
	id: number
	// userId: string
	firstName: string
	lastName: string
	age: number
}

const range = (len: number) => {
	const arr: number[] = []
	for (let i = 0; i < len; i++) {
		arr.push(i)
	}
	return arr
}

const newPerson = (num: number): Person => {
	return {
		id: num,
		// userId: faker.string.uuid(),
		firstName: faker.person.firstName(),
		lastName: faker.person.lastName(),
		age: faker.number.int(40),
	}
}

const ROW_HEIGHT = 35

const rows = range(2000).map((index): Person => newPerson(index))
const rowCount = rows.length

export const VirtualScroll = () => {
	const gridRef = useRef<HTMLDivElement>(null)

	const [scrollTop, setScrollTop] = useState(0)
	const [clientHeight, setClientHeight] = useState(0)

	const handleScroll = ({ currentTarget: { scrollTop } }: React.UIEvent<HTMLDivElement>) => setScrollTop(scrollTop)

	useEffect(() => {
		if (!gridRef.current) return
		const handleResize = () => {
			if (!gridRef.current) return
			setClientHeight(gridRef.current.clientHeight)
		}

		const { scrollTop, clientHeight } = gridRef.current

		setScrollTop(scrollTop)
		setClientHeight(clientHeight)
		window.addEventListener('resize', handleResize)

		return () => {
			window.removeEventListener('resize', handleResize)
		}
	}, [])

	const { startIndex, endIndex } = useMemo(() => {
		const viewportHeight = clientHeight
		const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT))
		const visibleRowCount = Math.min(rowCount - startIndex, Math.ceil(viewportHeight / ROW_HEIGHT))
		const endIndex = startIndex + visibleRowCount

		return { startIndex, endIndex }
	}, [clientHeight, scrollTop])

	const visibleRows = rows.slice(startIndex, endIndex)

	return (
		<div style={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
			{/* <div style={{ display: 'flex', gap: '1em' }}>
				<span>scrollTop: {scrollTop.toLocaleString()}px</span>
				<span>clientHeight: {clientHeight.toLocaleString()}px</span>
				<span>startNode: {startIndex.toLocaleString()}</span>
				<span>visibleNodesCount: {endIndex - startIndex}</span>
			</div> */}
			<div
				ref={gridRef}
				style={{
					display: 'grid',
					blockSize: 350,
					flexGrow: 1,
					gridTemplateRows: `repeat(${rows.length}, 35px)`,
					overflow: 'auto',
					border: '1px solid gray',
				}}
				onScroll={handleScroll}
			>
				{visibleRows.map((item, i: number) => (
					<div
						key={item.id}
						style={{
							gridRowStart: startIndex + i + 1,
							display: 'flex',
							gap: '1em',
							height: ROW_HEIGHT,
							borderBottom: '1px solid gray',
						}}
					>
						<span>{i + 1}</span>
						<span>{item.id}</span>
						{/* <span>{item.userId}</span> */}
						<span>{item.firstName}</span>
						<span>{item.lastName}</span>
						<span>{item.age}</span>
					</div>
				))}
			</div>
		</div>
	)
}

브라우저 resizing 까지는 대응했지만, debounce 미적용이라...

+ Recent posts