Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

react hooks rewrite #86

Open
roeycohen opened this issue Jul 7, 2021 · 10 comments
Open

react hooks rewrite #86

roeycohen opened this issue Jul 7, 2021 · 10 comments

Comments

@roeycohen
Copy link

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

@mmontag
Copy link

mmontag commented Sep 17, 2021

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

@roeycohen
Copy link
Author

roeycohen commented Sep 17, 2021

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
Copy link
Author

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
Copy link
Author

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

@wuarmin
Copy link

wuarmin commented Jun 3, 2022

Does it work with tbody?

@roeycohen
Copy link
Author

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

@wuarmin
Copy link

wuarmin commented Jun 4, 2022

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

@roeycohen
Copy link
Author

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

@wuarmin
Copy link

wuarmin commented Jun 25, 2022

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!

@mmontag
Copy link

mmontag commented May 26, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants