Skip to content

Instantly share code, notes, and snippets.

@luizeboli
Created July 11, 2022 13:25
Show Gist options
  • Save luizeboli/f0fb4ff2d2ac91e55e9a3b904af6e8d9 to your computer and use it in GitHub Desktop.
Save luizeboli/f0fb4ff2d2ac91e55e9a3b904af6e8d9 to your computer and use it in GitHub Desktop.

Revisions

  1. luizeboli created this gist Jul 11, 2022.
    127 changes: 127 additions & 0 deletions virtualized-list.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,127 @@
    import React, { useEffect, useReducer, memo, useRef } from "react";
    import debounce from "lodash.debounce";

    const VirtualizedList = ({
    itemHeight,
    getItemsToRender,
    overscan,
    renderItem,
    totalItems,
    parentRef,
    array
    }) => {
    const containerRef = useRef();
    const [state, dispatch] = useReducer(
    (prevState, nextState) => ({ ...prevState, ...nextState }),
    {
    amountToRender: 0,
    innerHeight: totalItems * itemHeight,
    topContainerHeight: 0,
    bottomContainerHeight: 0,
    itemsToRender: [],
    previousIndex: 0
    }
    );

    const onScroll = ({ target: { scrollTop } }) => {
    const { innerHeight } = state;

    const amountToRender = Math.round(
    parentRef.current.clientHeight / itemHeight + 2 * overscan
    );
    const startIndex = Math.max(
    0,
    Math.floor(scrollTop / itemHeight) - overscan
    );

    if (
    amountToRender === state.amountToRender &&
    state.previousIndex === startIndex
    )
    return;

    const itemsToRender = getItemsToRender(startIndex, amountToRender, array);
    const topContainerHeight = Math.max(startIndex * itemHeight, 0);
    const bottomContainerHeight = Math.max(
    innerHeight - topContainerHeight - amountToRender * itemHeight,
    0
    );

    dispatch({
    amountToRender,
    previousIndex: startIndex,
    itemsToRender,
    topContainerHeight,
    bottomContainerHeight,
    windowHeight: window.innerHeight
    });
    };

    useEffect(() => {
    onScroll({ target: { scrollTop: containerRef.current.scrollTop } });
    }, []);

    useEffect(() => {
    const debouncedScroll = debounce(
    () => onScroll({ target: { scrollTop: containerRef.current.scrollTop } }),
    200
    );

    const resizeListener = () => {
    if (state.windowHeight !== window.innerHeight) {
    debouncedScroll();
    }
    };

    window.addEventListener("resize", resizeListener);
    return () => window.removeEventListener("resize", resizeListener);
    }, [state.windowHeight]);

    useEffect(() => console.log("Virtual Rendered"));

    return (
    <div
    onScroll={onScroll}
    style={{ height: parentRef?.current?.clientHeight, overflowY: "scroll" }}
    ref={containerRef}
    >
    <div style={{ height: state.topContainerHeight }} />
    {state.itemsToRender?.map(renderItem)}
    <div style={{ height: state.bottomContainerHeight }} />
    </div>
    );
    };

    export default memo(VirtualizedList);

    // How to use

    // Function used by <VirtualizedList />
    // It'll receive the array, index and quantity to render
    // It should return items to render
    const getItemsToRender = (index, amountToRender, data) => {
    const items = [];

    const startIndex = Math.max(0, index);
    const endIndex = Math.min(index + amountToRender, data.length);

    for (let i = startIndex; i < endIndex; i += 1) {
    items.push(data[i]);
    }

    return items;
    };

    <VirtualizedList
    array={array}
    totalItems={array.length}
    itemHeight={25}
    overscan={10}
    getItemsToRender={getItemsToRender}
    renderItem={item => (
    <div key={item.Name} style={{ height: 25, padding: "0 2rem" }}>
    {item.Name}
    </div>
    )}
    parentRef={parentRef}
    />