Autodesk / react-base-table

A react table component to display large datasets with high performance and flexibility
https://autodesk.github.io/react-base-table/
MIT License
1.5k stars 164 forks source link

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

Open dingchaoyan1983 opened 5 years ago

dingchaoyan1983 commented 5 years ago

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

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?

nihgwu commented 5 years ago

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

dingchaoyan1983 commented 5 years ago

which Grid? the Grid in react-windowing?

nihgwu commented 5 years ago

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

dingchaoyan1983 commented 5 years ago

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

nihgwu commented 5 years ago

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

dingchaoyan1983 commented 5 years ago

ok, I will try

perepelitca commented 4 years ago

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

sgilligan commented 4 years ago

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

nihgwu commented 4 years ago

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

sgilligan commented 4 years ago

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?

nihgwu commented 4 years ago

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

sgilligan commented 4 years ago

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

nihgwu commented 4 years ago

@sgilligan you can get offset via onScroll

sgilligan commented 4 years ago

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

nihgwu commented 4 years ago

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

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
sgilligan commented 4 years ago

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

nihgwu commented 4 years ago

@sgilligan I updated the demo to address note 3

sgilligan commented 4 years ago

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

GregoryPotdevin commented 2 years ago

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 ?

conraddamon commented 1 year ago

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.

anhdd-kuro commented 1 year ago

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