react-virtual-list
react-virtual-list copied to clipboard
react hooks rewrite
Hi @developerdizzle,
I've reorganized this library code using hooks and react 17. I'm willing to share if you want to update this library...
Roey
Can you share your fork? Would love to get rid of some console warnings.
hi @mmontag, I don't have it in a fork, but here's the code (if you could create a fork for the community it could be nice 😊):
import React, {memo, useEffect, useRef, useState} from "react";
import PropTypes from 'prop-types';
export const VirtualList = memo(function VirtualList({items = [], itemHeight, itemBuffer, container = window, InnerComponent})
{
const listRef = useRef();
const [range, setRange] = useState({firstItemIndex: 0, lastItemIndex: -1});
let refreshState = () =>
{
if (!listRef.current)
return;
const newRange = getVisibleItemBounds(listRef.current, container, items, itemHeight, itemBuffer);
if (!newRange || newRange.firstItemIndex > newRange.lastItemIndex)
return;
if (newRange.firstItemIndex !== range.firstItemIndex || newRange.lastItemIndex !== range.lastItemIndex)
setRange(newRange);
};
if (typeof window !== 'undefined' && 'requestAnimationFrame' in window)
refreshState = throttleWithRAF(refreshState);
useEffect(refreshState);
useEventListener('scroll', refreshState, container);
useEventListener('resize', refreshState, container);
return <InnerComponent
listRef={listRef}
partialList={range.lastItemIndex > -1 ? items.slice(range.firstItemIndex, range.lastItemIndex + 1) : []}
style={{
height: items.length * itemHeight,
paddingTop: range.firstItemIndex * itemHeight,
boxSizing: 'border-box'
}}/>;
}, (prevProps, nextProps) =>
{
return prevProps.items === nextProps.items &&
prevProps.itemBuffer === nextProps.itemBuffer &&
prevProps.itemHeight === nextProps.itemHeight &&
prevProps.container === nextProps.container;
});
VirtualList.propTypes = {
InnerComponent: PropTypes.func,
container: PropTypes.any,
itemBuffer: PropTypes.number,
itemHeight: PropTypes.number,
items: PropTypes.array,
};
const getVisibleItemBounds = (list, container, items, itemHeight, itemBuffer) =>
{
// early return if we can't calculate
if (!container || !itemHeight || !items || items.length === 0)
return undefined;
// what the user can see
const {innerHeight, clientHeight} = container;
const viewHeight = innerHeight || clientHeight; // how many pixels are visible
if (!viewHeight)
return undefined;
const viewTop = getElementTop(container); // top y-coordinate of viewport inside container
const viewBottom = viewTop + viewHeight;
const listTop = topFromWindow(list) - topFromWindow(container); // top y-coordinate of container inside window
const listHeight = itemHeight * items.length;
// visible list inside view
const listViewTop = Math.max(0, viewTop - listTop); // top y-coordinate of list that is visible inside view
const listViewBottom = Math.max(0, Math.min(listHeight, viewBottom - listTop)); // bottom y-coordinate of list that is visible inside view
// visible item indexes
const firstItemIndex = Math.max(0, Math.floor(listViewTop / itemHeight) - itemBuffer);
const lastItemIndex = Math.min(items.length, Math.ceil(listViewBottom / itemHeight) + itemBuffer) - 1;
return {firstItemIndex, lastItemIndex};
};
const topFromWindow = (element) =>
{
if (typeof element === 'undefined' || !element)
return 0;
return (element.offsetTop || 0) + topFromWindow(element.offsetParent);
};
const getElementTop = (element) =>
{
if (element.pageYOffset)
return element.pageYOffset;
if (element.document)
{
if (element.document.documentElement && element.document.documentElement.scrollTop)
return element.document.documentElement.scrollTop;
if (element.document.body && element.document.body.scrollTop)
return element.document.body.scrollTop;
return 0;
}
return element.scrollY || element.scrollTop || 0;
};
const throttleWithRAF = function (fn)
{
let running = false;
return () =>
{
if (running)
return;
running = true;
window.requestAnimationFrame(() =>
{
fn.apply(this, arguments);
running = false;
});
};
};
// based on: https://usehooks.com/useEventListener/
const useEventListener = (eventName, handler, element = window) =>
{
const savedHandler = useRef();
useEffect(() => void (savedHandler.current = handler), [handler]);
useEffect(
() =>
{
const eventListener = event => savedHandler.current(event);
element.addEventListener(eventName, eventListener);
return () => element.removeEventListener(eventName, eventListener);
},
[eventName, element] // Re-run if eventName or element changes
);
};
also, here's a sample code on how to use it (I've extracted it from my project, hope it will help you although i'm using it with tables and not lists):
const innerTableComponent = useCallback(({partialList, style, listRef}) =>
<div css={styleTableList(compact)}>
<table style={style} ref={listRef}>
{thead}
<tbody>
{partialList.map(r => <ErrorBoundary key={r[rowIdCol]}><RowRenderer row={r} rowProps={rowProps}/></ErrorBoundary>)}
<tr style={{height: 'auto'}}/>
</tbody>
</table>
</div>
);
return <VirtualList
items={_collection}
itemHeight={50}
itemBuffer={15}
container={scrollParent || window}
InnerComponent={innerTableComponent}
/>;
hi @mmontag, let me know if this code helped you... or if you have any comments.
Does it work with tbody?
hi @wuarmin, we're using it with Tables if that's what you're asking...
Thanks @roeycohen ! Yeah that's my question. I want to use react table and need a working virtualization for tbody, but I need to use html table markup too. WDYT? Best regards
hi @wuarmin, sorry for my late reply... please take a look on my comment from Sep 17, 2021 above... it's a sample with a table :)
Hey @roeycohen!
Thanks. I tested your code, and I'm close to making it work. One question:
I have a container with 400px
height. In my Testcase I have 100 rows with height 26px
, so table has a height of 2600px
. If I scroll down, some upper rows (still in dom) are rendered above the viewport and so they are not visible:
The
padding-top
of table has no effect on collapsed tables. I have to set border
to separate
, but that is no option for me. Do you have an idea, how to solve this?
Thanks!
@roeycohen it didn't work for me since it looks like you changed the interface a bit. However, #78 works great without any codechange and would be nice to merge @developerdizzle.