developerdizzle / react-virtual-list

Super simple virtualized list React component
http://developerdizzle.github.io/react-virtual-list/
MIT License
617 stars 71 forks source link

react hooks rewrite #86

Open roeycohen opened 3 years ago

roeycohen commented 3 years ago

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 commented 3 years ago

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

roeycohen commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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

wuarmin commented 2 years ago

Does it work with tbody?

roeycohen commented 2 years ago

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

wuarmin commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 1 year ago

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