react-virtual-list icon indicating copy to clipboard operation
react-virtual-list copied to clipboard

react hooks rewrite

Open roeycohen opened this issue 3 years ago • 10 comments

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

roeycohen avatar Jul 07 '21 09:07 roeycohen

Can you share your fork? Would love to get rid of some console warnings.

mmontag avatar Sep 17 '21 09:09 mmontag

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
	);
};

roeycohen avatar Sep 17 '21 09:09 roeycohen

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}
/>;

roeycohen avatar Sep 17 '21 09:09 roeycohen

hi @mmontag, let me know if this code helped you... or if you have any comments.

roeycohen avatar Sep 28 '21 20:09 roeycohen

Does it work with tbody?

wuarmin avatar Jun 03 '22 10:06 wuarmin

hi @wuarmin, we're using it with Tables if that's what you're asking...

roeycohen avatar Jun 04 '22 07:06 roeycohen

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

wuarmin avatar Jun 04 '22 17:06 wuarmin

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 :)

roeycohen avatar Jun 18 '22 10:06 roeycohen

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: Peek 2022-06-25 19-07 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!

wuarmin avatar Jun 25 '22 16:06 wuarmin

@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.

mmontag avatar May 26 '23 09:05 mmontag