Skip to content

Instantly share code, notes, and snippets.

@egorguscha
Created October 5, 2020 07:50
Show Gist options
  • Save egorguscha/551e06c44cb1b58d9ede433334f46dbb to your computer and use it in GitHub Desktop.
Save egorguscha/551e06c44cb1b58d9ede433334f46dbb to your computer and use it in GitHub Desktop.
const setInitialState = (settings) => {
const {
itemHeight,
amount,
tolerance,
minIndex,
maxIndex,
startIndex
} = settings;
const viewportHeight = amount * itemHeight;
const totalHeight = (maxIndex - minIndex + 1) * itemHeight;
const toleranceHeight = tolerance * itemHeight;
const bufferHeight = viewportHeight + 2 * toleranceHeight;
const bufferedItems = amount + 2 * tolerance;
const itemsAbove = startIndex - tolerance - minIndex;
const topPaddingHeight = itemsAbove * itemHeight;
const bottomPaddingHeight = totalHeight - topPaddingHeight;
const initialPosition = topPaddingHeight + toleranceHeight;
return {
settings,
viewportHeight,
totalHeight,
toleranceHeight,
bufferHeight,
bufferedItems,
topPaddingHeight,
bottomPaddingHeight,
initialPosition,
data: []
};
};
function VirtualScroller({settings, get, row}) {
const [state, setState] = useState(() => setInitialState(settings));
let viewportRef = useRef(null);
const debounced = useCallback(debounce(({target: {scrollTop}}) => {
const {
totalHeight,
toleranceHeight,
bufferedItems,
settings: {itemHeight, minIndex}
} = state;
const index =
minIndex + Math.floor((scrollTop - toleranceHeight) / itemHeight);
const topPaddingHeight = Math.max((index - minIndex) * itemHeight, 0);
const data = get(index, bufferedItems);
data.then(data => {
const bottomPaddingHeight = Math.max(
totalHeight - topPaddingHeight - data.length * itemHeight,
0
);
setState({
...state,
topPaddingHeight,
bottomPaddingHeight,
data
});
})
}), [])
const runScroller = useCallback((event) => {
debounced({target: {scrollTop: event.target.scrollTop}})
}, [])
useLayoutEffect(() => {
viewportRef.current.scrollTop = state.initialPosition;
if (!state.initialPosition) {
runScroller({target: {scrollTop: 0}});
}
}, []);
const {viewportHeight, topPaddingHeight, bottomPaddingHeight, totalHeight, data} = state;
return (
<div
className="viewport"
ref={viewportRef}
onScroll={runScroller}
style={{height: viewportHeight, overflowY: 'auto'}}
>
<div
style={{
height: totalHeight
}}
>
<div style={{height: topPaddingHeight}}/>
{data && data.map(row)}
<div style={{height: bottomPaddingHeight}}/>
</div>
</div>
);
}
const SETTINGS = {
itemHeight: 20,
amount: 10,
tolerance: 2,
minIndex: 0,
maxIndex: 100,
startIndex: 0
};
const getData = (offset, limit) => {
const data = [];
const start = Math.max(SETTINGS.minIndex, offset);
const end = Math.min(offset + limit - 1, SETTINGS.maxIndex);
if (start <= end) {
for (let i = start; i <= end; i++) {
data.push({index: i, text: `item ${i}`});
}
}
return new Promise(res => {
setTimeout(res, 2000, data)
})
};
const rowTemplate = (item) => (
<div className="item" key={item.index}>
{item.text}
</div>
);
const AppComponent = () => (
<VirtualScroller
get={getData}
settings={SETTINGS}
row={rowTemplate}
/>
);
export const App = () => <AppComponent/>
export function debounce(fn, delay, immediate) {
let timer
return function () {
let ctx = this
let args = arguments
function delayFn() {
timer = null
if (!immediate) fn.apply(ctx, args)
}
let callNow = immediate && !timer
clearTimeout(timer)
timer = setTimeout(delayFn, delay)
if (callNow) {
fn.apply(ctx, args)
}
requestAnimationFrame(delayFn)
}
}
import React from 'react'
import ReactDOM from 'react-dom'
import {App} from './app'
const root = document.getElementById('root')
ReactDOM.render(<App/>, root)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment