高性能和高灵活度的react表格组件

对于组件导致的性能问题,的确令我十分头疼,后面我思考了两天一夜,最终才决定使用这个方案,这个方案或许可能是对于二维数组渲染的最优解了,的确目前暂时想不出更好的解决方案了,最后使用的是双重渲染和虚拟滚动,但考虑到可能对于数据量不大的场景,可能虚拟滚动并不是一个很好的选择,为此我保留了之前的渲染模式,留给大家自己决定,你可以选择打开虚拟滚动或者关闭

该react表格经过了大幅度优化,现用法如下: `

ini 复制代码
const defaultTableData = [ 
['姓名', '年龄', '职位', '入职日期'], 
['张三', '28', '前端开发工程师', '2022-05-15'], 
['李四', '32', '产品经理', '2021-08-23'],
['王五', '45', '技术总监', '2019-11-10'], 
['赵六', '35', 'UI设计师', '2023-01-05'] 
]; 


      <Table
        Item={data}           //表格数据
        virtualScroll={true}  //是否开启虚拟滚动
        rowHeight ={ 48}      //行高
        overscan ={5}          //预渲染行数
        className="w-full overflow-auto"        // 自定义样式
      />

`

原来的组件源码如下:

`

typescript 复制代码
import React, { forwardRef } from 'react';

export interface TableProps extends React.HTMLAttributes<HTMLDivElement> {
  className?: string;
  variant?: 'default' | 'bordered' | 'striped';
}


export interface TableBodyProps extends React.HTMLAttributes<HTMLDivElement> {
  Item: (number|string|React.ReactNode)[][]|HTMLDivElement[];
  className?: string;
}

export const Table = forwardRef<HTMLDivElement, TableProps>(
  ({ className, variant = 'default', children, ...props }, ref) => {
    const baseClasses = "w-full overflow-hidden";
    
    const variantClasses = {
      default: "",
      bordered: "border border-gray-200 dark:border-gray-800 rounded-lg",
      striped: ""
    };
    
    const classes = [
      baseClasses,
      variantClasses[variant],
      className
    ].filter(Boolean).join(' ');

    return (
      <div 
        ref={ref}
        className={classes}
        data-table="root"
        {...props}
      >
        {children}
      </div>
    );
  }
);

Table.displayName = 'Table';



export const TableBody = forwardRef<HTMLDivElement, TableBodyProps>(
  ({ className, Item, ...props }, ref) => {
    const classes = [
      "w-full",
      className
    ].filter(Boolean).join(' ');

    const formatItems = () => {
      if (!Array.isArray(Item)) {
        return [];
      }
      
      if (Array.isArray(Item[0])) {
        return Item;
      }
      
      return [[...(Item as unknown as (number|string)[])]];
    };

    const formattedItems = formatItems();

    return (
      <div ref={ref} className={classes} data-table="body" {...props}>
        <table className="w-full">
          <tbody>
            {formattedItems.map((row, rowIndex) => (
              <tr 
                key={rowIndex} 
                className="border-b  border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors"
              >
                {Array.isArray(row) ? row.map((cell, cellIndex) => (
                  <td 
                    key={cellIndex} 
                    className="py-3 px-4 text-gray-700 dark:text-gray-300"
                  >
                    {cell}
                  </td>
                )):''}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  }
);

TableBody.displayName = 'TableBody';

`

可以看到,这里并没有使用任何的优化手段,只是单纯的进行空判断后,直接进行二维数组渲染

以下是新的表格组件的源码: `

ini 复制代码
'use client'
import React, { forwardRef, useMemo, useCallback, useState, useEffect, useRef } from 'react';

interface TableBodyProps {
    className?: string;
    Item: (number | string | React.ReactNode)[][] | (number | string | React.ReactNode)[];
    rowHeight?: number;
    overscan?: number;
    virtualScroll?: boolean;
};

// 防抖hook 
const useDebounce = (value: number, delay: number) => {
    const [debouncedValue, setDebouncedValue] = useState(value)

    useEffect(() => {
        const handler = setTimeout(() => {
            setDebouncedValue(value);
        }, delay);

        return () => {
            clearTimeout(handler);
        };
    }, [value, delay]);

    return debouncedValue;
};

// 虚拟滚动组件
const VirtualRow = React.memo(({
    rowData, //数据
    className,
    ...props
}: {
    rowData: (number | string | React.ReactNode)[];
    className?: string;
}) => {
    return (
        <tr
            className={`border-b border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors ${className}`}
            {...props}
        >
            {rowData.map((cell, cellIndex) => (
                <td
                    key={cellIndex}
                    className={`py-3 px-4 text-gray-700 dark:text-gray-300`}
                >
                    {cell}
                </td>
            ))}
        </tr>
    );
});

VirtualRow.displayName = 'VirtualRow';


export const Table = forwardRef<HTMLDivElement, TableBodyProps>(
    ({
        className,                      //样式
        Item,                           // 二维数组表格数据
        rowHeight = 48,                 // 行高
        overscan = 5,                   // 预渲染行数
        virtualScroll = true,            //默认虚拟滚动
        ...props
    }, ref) => {
        const containerRef = useRef<HTMLDivElement>(null);           //容器元素Ref引用
        const [scrollTop, setScrollTop] = useState(0);               //滚动垂直位置
        const [containerHeight, setContainerHeight] = useState(0);   //容器高度
        const debouncedScrollTop = useDebounce(scrollTop, 16)        //防抖的滚动位置


        //空判断
        const flatData = useMemo(() => {
            if (!Array.isArray(Item)) return [];
            return Array.isArray(Item[0]) ? Item : [Item];
        }, [Item]);

        // 容器高度变化侦听
        useEffect(() => {
            const updateContainerHeight = () => {
                if (containerRef.current) {
                    setContainerHeight(containerRef.current.clientHeight);// clientHeight:客户端高度,也就是可见高度
                }
            };

            updateContainerHeight();


            const resizeObserver = new ResizeObserver(updateContainerHeight);
            if (containerRef.current) {
                resizeObserver.observe(containerRef.current);// 监听容器元素尺寸变化
            }

            return () => {
                resizeObserver.disconnect();// 断开侦听,清除资源
            };
        }, []);

        const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
            setScrollTop(event.currentTarget.scrollTop);// 侦听滚动条垂直位置变化
        }, []);

        // 计算可见范围
        const visibleRange = useMemo(() => {
            if (!virtualScroll || containerHeight === 0) {
                return { start: 0, end: flatData.length };// 空侦听
            }

            const startIndex = Math.max(0, Math.floor(debouncedScrollTop / rowHeight) - overscan);// 开始索引
            const visibleRowCount = Math.ceil(containerHeight / rowHeight);// 可见行
            const endIndex = Math.min(
                flatData.length,
                startIndex + visibleRowCount + overscan * 2
            );// 结束索引

            return { start: startIndex, end: endIndex };
        }, [debouncedScrollTop, containerHeight, rowHeight, overscan, virtualScroll, flatData.length]);
        const visibleRows = useMemo(() => {
            return flatData.slice(visibleRange.start, visibleRange.end);
        }, [flatData, visibleRange.start, visibleRange.end]);// 可见区域行


        const totalHeight = useMemo(() => {
            return flatData.length * rowHeight;
        }, [flatData.length, rowHeight])//容器内所有行总高度


        //className空判断后进行合并
        const classes = useMemo(() =>
            ["w-full overflow-auto", className].filter(Boolean).join(' '),  
            [className]
        )

        return (
            <div
                ref={containerRef}
                className={classes}
                onScroll={handleScroll}
                style={{ height: '100%', minHeight: '200px' }}
                {...props}
            >
                {virtualScroll ? (
                    <div
                        ref={ref}
                        style={{
                            position: 'relative',
                            height: totalHeight
                        }}
                    >
                        <table className="w-full" style={{ position: 'absolute', top: 0, left: 0, right: 0 }}>
                            <tbody>
                                {visibleRows.map((row, relativeIndex) => {
                                    const absoluteIndex = visibleRange.start + relativeIndex;

                                    return (
                                        <VirtualRow
                                            key={absoluteIndex}
                                            rowData={Array.isArray(row) ? row : [row]}
                                            className={className}
                                        />
                                    );
                                })}
                            </tbody>
                        </table>
                    </div>
                ) : (
                    <div ref={ref}>
                        <table className="w-full">
                            <tbody>
                                {flatData.map((row, rowIndex) => (
                                    <tr
                                        key={rowIndex}
                                        className={`border-b border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors ${className}`}
                                    >
                                        {Array.isArray(row) && row.map((cell, cellIndex) => (
                                            <td
                                                key={cellIndex}
                                                className={`py-3 px-4 text-gray-700 dark:text-gray-300 `}
                                            >
                                                {cell}
                                            </td>
                                        ))}
                                    </tr>
                                ))}
                            </tbody>
                        </table>
                    </div>
                )}
            </div>
        );
    }
);

`

这里主要使用了虚拟滚动来优化性能,当然,你也可以选择关闭虚拟滚动,避免过度优化

使用如下

javascript 复制代码
    const stripedTableData = [
        ['图片', '图片内容'],
        [
            <div key="vercel">
                <img src="/vercel.svg" alt="table" className='w-14 h-14 bg-black ' />
            </div>
            ,
            'vercel'
            ,
        ],
        [
            <div key="next">
                <img src="/next.svg" alt="table" />
            </div>
            ,
            'next.js'
            ,
        ],
        [
            <div key="file">
                <img src="/file.svg" alt="table" className='w-14 h-14 ' />
            </div>
            ,
            'flie'
            ,
        ],

    ];



<Table className="rounded-lg overflow-hidden">
    <TableBody
        Item={stripedTableData}
        className="bg-card"
    />
</Table>

组件十分灵活,而且上手简单,你可以自由选择写正常数据或者给某一个单元格定制化而写入jsx,你也可以做一些表格的图片和视频展示和各种个性化的表格需求,也可以数据和jsx混合使用,这都在允许的范围

表格的组件进行了优化后,使它接近了像ant的表格组件的开发体验,甚至可能比ant表格组件更加简便,但是一样不失他本身独有的灵活性,但表格可能功能不如ant完善,需要逐步添加

但新表格的源码有点多,可能需要单独写一篇了,这里不详细介绍了,其实我标了注释了,只要认真看一下也知道新组件的作用的什么的

这里也感谢你可以看到这样,下次见了,拜拜!

相关推荐
崔庆才丨静觅几秒前
稳定好用的 ADSL 拨号代理,就这家了!
前端
江湖有缘2 分钟前
Docker部署music-tag-web音乐标签编辑器
前端·docker·编辑器
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端