react-base-table icon indicating copy to clipboard operation
react-base-table copied to clipboard

The table dose not support lazy render on columns (horizontal level)

Open dingchaoyan1983 opened this issue 5 years ago • 23 comments

assuming I have 100 columns data, the 100 columns will rendered at once. can we support this feature of lazy render in column level?

dingchaoyan1983 avatar May 14 '19 06:05 dingchaoyan1983

That is because here

    var expandIcon = expandIconRenderer({
      rowData: rowData,
      rowIndex: rowIndex,
      depth: depth,
      onExpand: this._handleExpand
    });
    var cells = columns.map(function (column, columnIndex) {
      return cellRenderer({
        isScrolling: isScrolling,
        columns: columns,
        column: column,
        columnIndex: columnIndex,
        rowData: rowData,
        rowIndex: rowIndex,
        expandIcon: column.key === expandColumnKey && expandIcon
      });
    });

    if (rowRenderer) {
      cells = renderElement(rowRenderer, {
        isScrolling: isScrolling,
        cells: cells,
        columns: columns,
        rowData: rowData,
        rowIndex: rowIndex,
        depth: depth
      });
    }

when render a row, we render the all columns can we improve this?

dingchaoyan1983 avatar May 14 '19 07:05 dingchaoyan1983

In that case I think you should consider to use a Grid instead of Table, but I think that's a interesting feature we could add in the future

nihgwu avatar May 14 '19 07:05 nihgwu

which Grid? the Grid in react-windowing?

dingchaoyan1983 avatar May 14 '19 08:05 dingchaoyan1983

https://react-window.now.sh/#/examples/grid/fixed-size

nihgwu avatar May 14 '19 08:05 nihgwu

So I can not use react-base-table, I need to implement such as fixed myself, sad

dingchaoyan1983 avatar May 14 '19 08:05 dingchaoyan1983

or you could send a pr to add virtualization support for columns in BaseTable

nihgwu avatar May 14 '19 09:05 nihgwu

ok, I will try

dingchaoyan1983 avatar May 14 '19 10:05 dingchaoyan1983

Hey, @dingchaoyan1983! Did you have a chance to take a look columns virtualization?

perepelitca avatar Jan 06 '20 22:01 perepelitca

+1.

I ported from a 20+ column react-virtualized multigrid (with plenty of custom renderers) to react-base-table, assuming the use of react-window would include column virtualisation as well as row virtualisation. I discover my assumption to be wrong! I now have a noticeable performance drop in scrolling. No complaint here - my fault, I should have done more research - RBT adds some amazing capability on top of react-window, but to have this would make it complete :)

sgilligan avatar Aug 04 '20 09:08 sgilligan

one of my thoughts is that if we have too many columns, it's a Grid not Table, there is no concept of Row actually, only Cell

nihgwu avatar Aug 04 '20 13:08 nihgwu

In my context, I'd still call it table. Each row relates to a singular entity, each column is an attribute of that entity. Fixed headers/columns/sortable (etc) all conceptually make sense. But arguably, perhaps 20+ columns is too many?

sgilligan avatar Aug 05 '20 00:08 sgilligan

Internally we have table with more then 30 columns, but only few custom columns

nihgwu avatar Aug 05 '20 01:08 nihgwu

@nihgwu Within the TableRow render function, is there a way to find the offset at which the row is currently horizontally scrolled to? I'm messing around with a crude column virtualisation that might help with my specifics (by the way, my table is 10,000 rows, 23 columns and performance is sorta ok with Chrome but really bogs down with Firefox on macOS).

sgilligan avatar Aug 05 '20 05:08 sgilligan

@sgilligan you can get offset via onScroll

nihgwu avatar Aug 05 '20 08:08 nihgwu

@nihgwu this is an experiment with trying to lazy render columns. The simple approach is to not render non-visible columns when scrolling vertically, but show them all when scrolling horizontally. In my own context, it really helps with vertical scrolling performance on firefox/macos, but on first horizontal movement the columns remain un-rendered until the scroll finishes. Is this a viable approach?

sgilligan avatar Aug 06 '20 06:08 sgilligan

the reason for the blank cells when scroll horizontally is that BaseTable optimized to avoid unnecessary re-renders, there is no internal state changed(more specific, either data or columns should be changed to trigger re-render

nihgwu avatar Aug 06 '20 16:08 nihgwu

here is a workable demo modified from yours, some notes:

  1. change children directly takes no effect
  2. you can calculate the visible start and end column index and only update the rowRenderer is the two indices changed, and use those two indices instead of scrollLeft to trigger re-render
  3. you can merge the invisible cells into one, perhaps that would improve the performance a bit, the example may help

nihgwu avatar Aug 06 '20 16:08 nihgwu

aha! thanks so much @nihgwu, that gives me plenty to work with. This is much appreciated and thanks for your outstanding engagement.

sgilligan avatar Aug 07 '20 10:08 sgilligan

@sgilligan I updated the demo to address note 3

nihgwu avatar Aug 07 '20 11:08 nihgwu

@nihgwu ahh .. yes I understand now. That's a clear improvement - will use that - thanks again

sgilligan avatar Aug 08 '20 09:08 sgilligan

here is a workable demo

  1. change children directly takes no effect
  2. you can calculate the visible start and end column index and only update the rowRenderer is the two indices changed, and use those two indices instead of scrollLeft to trigger re-render
  3. you can merge the invisible cells into one, perhaps that would improve the performance a bit, the example may help

Thanks, we implemented this for our backoffice (some tables have 10k lines and 50+ columns) and the table feels much more responsive now.

Maybe this code sample could be added to the standard examples on the website ?

GregoryPotdevin avatar Jul 06 '22 22:07 GregoryPotdevin

I was able to address this in our component by adjusting the list of columns based on the viewport width and the value of scrollLeft. Since we know the widths of all our columns, it was pretty easy to calculate those that are visible, plus one page (viewport width) on either side. Then I added filler elements to the left and right to make the scrolling look and work right.

conraddamon avatar Feb 02 '23 19:02 conraddamon

For anyone who looking for solution now because the lib have changed a lot Here's my example code using custom hook & typescript this code writing on version 1.13.4 and base on nihgwu's work around

typescript issue : #407

/**
 * ? This hook is used to support virtualization on horizontal scrolling because react-base-table does not support it out of the box.
 *
 * ? The idea is to nullify the cells that are not visible in the viewport. This is done by using the `rowRenderer` prop of react-base-table.
 */
export const useHorizontalVirtualList = ({
  initialScrollLeft = 0,
  tableWidth,
}: {
  initialScrollLeft?: number;
  tableWidth: number;
}) => {
  const [scrollLeft, setScrollLeft] = useState(initialScrollLeft);

  const onScroll = useCallback<CVirtualListTableOnScroll>(
    (args) => {
      if (args.scrollLeft !== scrollLeft) {
        setScrollLeft(args.scrollLeft);
      }
    },
    [scrollLeft],
  );

  const rowRenderer: BaseTableProps["rowRenderer"] = ({ cells, columns, rowIndex }) => {
    // columns type should be array , this code writing on version 1.13.4
    // this check is just for satisfy ts
    if (Array.isArray(columns)) {
      const notFrozenColumns: ColumnShape[] = columns.filter((col) => !col.frozen);

      // minus the frozen col right to get actual visible range
      const frozenRightColumnsWidth: number = columns
        .filter((col: ColumnShape) => col.frozen === "right")
        .reduce((acc, col: ColumnShape) => acc + col.width, 0);

      const { outside } = getColumnVisibility({
        offset: scrollLeft,
        frozenRightColumnsWidth,
        columns: notFrozenColumns,
        tableWidth,
      });

      outside.forEach((colIdx) => {
        const cell = cells[colIdx];
        if (isValidElement(cell)) {
          cells[colIdx] = cloneElement(cell, {}, null);
        }
      });
    }

    return cells;
  };

  return {
    onScroll,
    rowRenderer,
    scrollLeft,
  };
};

const getColumnVisibility = ({
  offset,
  columns,
  tableWidth,
  frozenRightColumnsWidth,
}: {
  offset: number;
  columns: ColumnShape[];
  tableWidth: number;
  frozenRightColumnsWidth: number;
}) => {
  // build the net offset for each column
  const netOffsets: number[] = [];

  let offsetSum = 0;
  const leftBound = offset;
  const rightBound = offset + tableWidth - frozenRightColumnsWidth;
  const outside: number[] = [];
  const inside: number[] = [];

  columns.forEach((col) => {
    netOffsets.push(offsetSum);
    offsetSum += col.width;
  });

  // which column offsets are outside the left and right bounds?
  netOffsets.forEach((columnOffset, colIdx) => {
    const isNotVisible = columnOffset < leftBound || columnOffset > rightBound;

    if (isNotVisible) {
      outside.push(colIdx);
    } else {
      inside.push(colIdx);
    }
  });

  return {
    outside,
    inside,
  };
};

Usage

import BaseTable, { AutoResizer } from "react-base-table";
import type { BaseTableProps, ColumnShape } from "react-base-table";

type BaseRow = {
  id: string;
  parentId?: string | null;
};

export type CVirtualListTableProps = {
  columns: CVirtualListTableColumnShape[];
  rows: BaseRow[];
  initialScroll?: {
    x?: number;
    y?: number;
  };
} & Partial<
  Pick<BaseTableProps, "fixed" | "headerRenderer" | "cellRenderer" | "height" | "onScroll">
>;

export type CVirtualListTableOnScroll = TypeHelpers.NonNullUndefined<BaseTableProps["onScroll"]>;

export const CVirtualListTable = forwardRef<BaseTable, BaseTableProps>(
  function CVirtualListTableForwardedRef(props: CVirtualListTableProps, ref?: Ref<BaseTable>) {
    const {
      columns,
      rows,
      initialScroll,
      height,
      onScroll: onScrollProp,
      ...otherProps
    } = props;
    const tableWidth = useRef(0);

    const { onScroll, rowRenderer } = useHorizontalVirtualList({
      initialScrollLeft: initialScroll?.x,
      tableWidth: tableWidth.current,
    });

    return (
      <AutoResizer height={height}>
        {({ width, height: autoHeight }) => {
          tableWidth.current = width;

          return (
            <BaseTable
              width={width}
              columns={columns}
              data={rows}
              height={autoHeight}
              rowRenderer={rowRenderer}
              onScroll={(args) => {
                onScrollProp?.(args);
                onScroll(args);
              }}
              {...otherProps}
            />
          );
        }}
      </AutoResizer>
    );
  },
);

anhdd-kuro avatar Mar 19 '23 09:03 anhdd-kuro