发布于 2026-01-06 0 阅读
0

React智能数据表构建完整指南

React智能数据表构建完整指南

作者:帕拉马南塔姆·哈里森✏️

表格型用户界面在网络产品中非常常见,因为它是在用户界面中组织复杂数据的最简便方法之一。许多公司使用数据表来展示复杂的报表。

表格用户界面的一些常见用例包括显示财务报告数据、体育排行榜以及定价和比较页面。

数据表用户界面显示体育统计数据
体育统计数据表。

大量使用数据表的产品包括:

  • Airtable
  • Asana 列表视图
  • Asana 时间线
  • Google 表格
  • 概念表

表格用户界面功能

数据表用户界面的基本功能包括:

  • 毫不妥协的用户体验和用户界面。清晰易懂的字体和表格用户界面内的自定义元素。
  • 远程数据调用以加载数据
  • 搜索表格或特定列
  • 基本筛选和排序选项

数据表用户界面中的高级功能包括:

  • 根据数据类型(数字、字符串、布尔值、选择输入等)对列进行自定义排序和筛选选项
  • 分页支持或无限长表格(针对超大型数据集的性能)
  • 显示和隐藏列
  • 支持对列数据进行内联编辑
  • 支持通过模态/详细信息面板编辑整行数据。
  • 固定标题和固定列,便于查看数据。
  • 支持多设备(响应速度)
  • 可调整列宽,以便在列中容纳较长的数据点(例如,多行注释)
  • 支持水平和垂直滚动
  • 可展开的行,用于显示该行的完整数据

LogRocket 免费试用横幅

表格用户界面中常见的用户体验挑战

从用户界面角度来看,数据表是清晰呈现复杂数据的最佳选择之一。但从用户体验角度来看,它却很棘手——当需要支持多种设备时,很容易出现问题。表格在用户体验方面面临的一些挑战包括:

响应能力

如果不改变布局以适应较小的屏幕尺寸,就很难使表格具有响应式设计。

滚动

表格可能需要双向滚动。浏览器默认的滚动条对于全宽表格来说效果不错,但大多数表格的宽度都是自定义的。自定义滚动条在触摸屏和非触摸屏上的支持都非常棘手。

管理列宽

根据数据长度管理列宽是一项棘手的任务。当我们在表格中加载动态数据时,这常常会导致用户体验问题。每次数据更改时,列宽都会调整,从而造成对齐错位。我们在设计用户体验时需要谨慎处理这些问题。

React 中用于数据表的顶级库

本文将介绍如何使用 React 构建一个简单的 Airtable 克隆版。我们将探索一些开源的 React 表格​​库,并选择最适合我们用例的库。

react-table

React - table是 React 中最常用的表格库之一。它在 GitHub 上拥有超过 7000 个 star,更新频繁,并支持 Hooks。React-table 非常轻量级,提供了任何简单表格所需的所有基本功能

何时使用 react-table

当您的表格用户界面需要:

  • 排序、筛选和分页等基本功能
  • 在不影响功能的前提下,为表格设计自定义用户界面。
  • 易于扩展;您可以使用自定义插件钩子在库的基础上构建自己的功能。

何时不应使用 react-table

当您需要时:

  • 默认支持固定标题和列
  • react-table 开箱即用,支持触控和非触控设备的水平和垂直滚动。它不定义用户界面;它是无头框架,因此我们需要根据自身需求来定义用户界面。
  • 支持列的内联编辑。我们可以在 react-table 中实现这一点,但这超出了我们表格本身的功能范围。我们需要在其基础上创建一个插件或组件来支持此类功能。react-table 名副其实,最适合渲染简单的表格。
  • 它无法处理像 Google 表格那样无限长的表格,性能方面也无法胜任。对于中等大小的表格,它表现良好,但对于过长的表格则不然。

用例

  • 适用于需要搜索、排序、筛选等基本功能的简单表格。
  • 体育排行榜/统计数据,带有自定义元素的财务数据表

react-data-grid

react-data-grid是另一个用于创建智能表格的库。它在 GitHub 上拥有近 4000 个 star,并且维护良好。

何时使用 react-data-grid

当您的数据表需要:

  • 基本功能包括列分组、排序、搜索和筛选。
  • 列的内联编辑
  • 列内的下拉菜单(例如 Google 表格)或列内的任何自定义输入元素
  • 支持展开列以显示更多数据
  • 为了优化性能,例如,它支持无限长表格行的虚拟渲染。
  • 支持无行时的空状态

何时不应使用 react-data-grid

react-data-grid 几乎涵盖了数据表的所有基本需求。但是,它默认不支持分页,因此如果你的表格需要分页,你需要手动实现和处理。默认情况下,react-data-grid 支持较长的表格 UI,并且针对性能进行了优化,因此除非用户体验有特殊要求,否则分页可能并非必要。

它还使用 Bootstrap 进行样式设计。即使不使用 Bootstrap,你仍然可以使用 react-data-grid,但那样你就需要自己为表格添加样式。与 react-table 相比,它的可定制性较差,react-table 允许你创建表格结构。在 react-data-grid 中,表格 UI 由库自动生成,因此它不太适合 UI 繁重的自定义页面。

虽然以上几点并不算是缺点,但在开始使用 react-data-grid 之前了解它们还是很有帮助的。

用例

当你需要构建一个类似于 Google Sheets 或 Airtable 的小型可编辑数据表,并具有良好的用户体验时,中级需求。

react-datasheet

react - datasheet与 react-data-grid 类似。它们在 GitHub 上拥有相近的 star 数和贡献数,并且同样是一个维护良好的库

它主要专注于创建类似 Google Sheets 的应用程序。它内置了一些基本功能,可用于创建此类对用户体验要求较高的应用程序。但需要再次强调的是,它可能不适合创建包含表格的通用页面用户界面。

然而,与 react-data-grid 不同,它并未针对大型数据集进行优化,因此适用于需要类似 Sheets 功能的小型应用程序。它仅适用于这一种场景,并且与 react-data-grid 相比,其功能非常有限。

react-virtualized

顾名思义,react-virtualized在处理大型数据集时针对性能进行了深度优化。这个库并非严格意义上的表格库,它的功能远不止于此。它专门用于在用户界面上以不同的格式(例如网格、表格和列表)显示大型数据集。

我不会深入探讨这个库的细节,因为它功能远超我们的需求。

何时使用 react-virtualized

当数据量非常大时,表格的渲染性能是关键指标;在这种情况下,建议使用 react-virtualized。对于一般的使用场景,这个库就显得过于复杂,其 API 也过于高级。

用例

使用 react-virtualized 可以为大型数据集创建自定义时间线、包含无限长日历的图表以及复杂的 UI 元素。

那么,你应该选择哪个 React 表格​​库呢?

  • 对于数据量有限、样式自定义程度低、交互性仅需少量功能(例如排序和筛选)的简单页面,请使用react_–_table。
  • 要构建一个类似 Google Sheets 的迷你应用,但数据量有限,可以使用react-data-gridreact-datasheet。
  • 对于类似 Google Sheets 或 Airtable 且拥有大型数据集的应用程序,请使用react-data-grid
  • 当您处理非常大的数据集并且需要提供表格、网格以及更多选项的自定义 UI 时,请选择react-virtualized。

何时应该构建自己的表格用户界面

在某些情况下,您可能需要构建自己的表格用户界面:

  • 当你的餐桌只是一个展示柜,互动性不强时
  • 当您需要为表格定制用户界面时
  • 当你需要一张非常轻便且没有任何功能的桌子时

用例

  • 产品/营销页面,附有对比表格。
  • 价格表
  • 简单的表格,带有自定义样式,除了简单的弹出文本外,无需对列进行太多交互。

使用 React 构建智能表格 UI

理论就讲到这里——让我们开始构建一个简单的表格用户界面,它具备排序和搜索等基本功能react-table。我们将构建这个简单的表格,它拥有基本的搜索和排序功能。

首先,使用 create-react-app 创建一个 React 应用:

npx create-react-app react-table-demo
Enter fullscreen mode Exit fullscreen mode

调用 TV Maze API 获取表格数据

这是API接口。我们将调用该接口,并使用搜索词“雪”来获取节目信息。

为了调用 API,我们需要安装axios

yarn add axios
Enter fullscreen mode Exit fullscreen mode
// App.js

import React, { useState, useEffect } from "react";

import Table from "./Table";
import "./App.css";

function App() {
  // data state to store the TV Maze API data. Its initial value is an empty array
  const [data, setData] = useState([]);

  // Using useEffect to call the API once mounted and set the data
  useEffect(() => {
    (async () => {
      const result = await axios("https://api.tvmaze.com/search/shows?q=snow");
      setData(result.data);
    })();
  }, []);

  return (
    <div className="App"></div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

我们创建一个名为 的状态data,组件挂载后,我们使用 Axios 调用 API 并设置data

使用数据渲染一个简单的表格用户界面

现在我们将添加 react-table:

yarn add react-table
Enter fullscreen mode Exit fullscreen mode

react-table 使用 Hooks。它有一个名为 `table_hook` 的主表格 Hook useTable,并且还有一个插件系统,可以添加插件 Hooks。因此,react-table 可以根据我们的自定义需求轻松扩展。

让我们用 Hook 创建基本的 UI useTable。我们将创建一个新Table组件,它接受两个 props:` datadata` 和 ` columnscolumns`。`data`data是我们通过 API 调用获取的数据,columns`columns` 是用于定义表格列(表头、行、行的显示方式等)的对象。稍后我们将看到代码示例。

// Table.js

export default function Table({ columns, data }) {
// Table component logic and UI come here
}
Enter fullscreen mode Exit fullscreen mode
// App.js
import React, { useMemo, useState, useEffect } from "react";

import Table from "./Table";

function App() {

  /* 
    - Columns is a simple array right now, but it will contain some logic later on. It is recommended by react-table to Memoize the columns data
    - Here in this example, we have grouped our columns into two headers. react-table is flexible enough to create grouped table headers
  */
  const columns = useMemo(
    () => [
      {
        // first group - TV Show
        Header: "TV Show",
        // First group columns
        columns: [
          {
            Header: "Name",
            accessor: "show.name"
          },
          {
            Header: "Type",
            accessor: "show.type"
          }
        ]
      },
      {
        // Second group - Details
        Header: "Details",
        // Second group columns
        columns: [
          {
            Header: "Language",
            accessor: "show.language"
          },
          {
            Header: "Genre(s)",
            accessor: "show.genres"
          },
          {
            Header: "Runtime",
            accessor: "show.runtime"
          },
          {
            Header: "Status",
            accessor: "show.status"
          }
        ]
      }
    ],
    []
  );

  ...

  return (
    <div className="App">
      <Table columns={columns} data={data} />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

在这里,我们可以在列中创建多个表头和列组。我们创建了两级。

所有列都有一个访问器,用于访问data对象中的数据。我们的数据位于对象内部的show数组中——这就是为什么所有访问器都带有show.前缀的原因。

// sample data array looks like this

[
  {
    "score": 17.592657,
    "show": {
      "id": 44813,
      "url": "http://www.tvmaze.com/shows/44813/the-snow-spider",
      "name": "The Snow Spider",
      "type": "Scripted",
      "language": "English",
      "genres": [
        "Drama",
        "Fantasy"
      ],
      "status": "In Development",
      "runtime": 30,
      "premiered": null,
      "officialSite": null,
      "schedule": {
        "time": "",
        "days": [

        ]
      }
      ...
  },
  {
    // next TV show
  }
...
]
Enter fullscreen mode Exit fullscreen mode

让我们完成这个Table组件:

// Table.js

import React from "react";
import { useTable } from "react-table";

export default function Table({ columns, data }) {
  // Use the useTable Hook to send the columns and data to build the table
  const {
    getTableProps, // table props from react-table
    getTableBodyProps, // table body props from react-table
    headerGroups, // headerGroups if your table have groupings
    rows, // rows for the table based on the data passed
    prepareRow // Prepare the row (this function need to called for each row before getting the row props)
  } = useTable({
    columns,
    data
  });

  /* 
    Render the UI for your table
    - react-table doesn't have UI, it's headless. We just need to put the react-table props from the Hooks, and it will do its magic automatically
  */
  return (
    <table {...getTableProps()}>
      <thead>
        {headerGroups.map(headerGroup => (
          <tr {...headerGroup.getHeaderGroupProps()}>
            {headerGroup.headers.map(column => (
              <th {...column.getHeaderProps()}>{column.render("Header")}</th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody {...getTableBodyProps()}>
        {rows.map((row, i) => {
          prepareRow(row);
          return (
            <tr {...row.getRowProps()}>
              {row.cells.map(cell => {
                return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
              })}
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}
Enter fullscreen mode Exit fullscreen mode

我们将把 ` columnsand`传递datauseTable`。`。`。`。`。`。`。`。`。`。`。`。`。`。`。`。`表头`将通过遍历`,`。`。`。`。`。`。`。`。`。。表格headerGroups主体`行`将通过遍历`创建rows。` ...

{rows.map((row, i) => {
  prepareRow(row); // This line is necessary to prepare the rows and get the row props from react-table dynamically

  // Each row can be rendered directly as a string using the react-table render method
  return (
    <tr {...row.getRowProps()}>
      {row.cells.map(cell => {
        return <td {...cell.getCellProps()}>{cell.render("Cell")}</td>;
      })}
    </tr>
  );
})}
Enter fullscreen mode Exit fullscreen mode

这样,我们就渲染出了单元格和表头。但是我们的cell值只是字符串,即使是数组值也会被转换成逗号分隔的字符串值。例如:

// Genres array

show.genres = [
 'Comedy',
 'Sci-fi',
]
Enter fullscreen mode Exit fullscreen mode

在表格中,它将简单地显示为一个以逗号分隔的字符串,例如:Comedy,Sci-fi。此时,我们的应用程序应该看起来像这样:

功能有限的基本表格用户界面

react-table 中的自定义样式

对于大多数使用场景来说,这个表格已经足够好了,但如果我们需要自定义样式呢?react-table 允许你为每个单元格定义自定义样式。可以在column对象中这样定义。让我们创建一个类似徽章的自定义元素来显示每个类型。

// App.js

import React, { useMemo } from "react";
...

// Custom component to render Genres 
const Genres = ({ values }) => {
  // Loop through the array and create a badge-like component instead of comma-separated string
  return (
    <>
      {values.map((genre, idx) => {
        return (
          <span key={idx} className="badge">
            {genre}
          </span>
        );
      })}
    </>
  );
};

function App() {
  const columns = useMemo(
    () => [
      ...
      {
        Header: "Details",
        columns: [
          {
            Header: "Language",
            accessor: "show.language"
          },
          {
            Header: "Genre(s)",
            accessor: "show.genres",
            // Cell method will provide the cell value, we pass it to render a custom component
            Cell: ({ cell: { value } }) => <Genres values={value} />
          },
          {
            Header: "Runtime",
            accessor: "show.runtime",
            // Cell method will provide the value of the cell, we can create custom element for the Cell        
            Cell: ({ cell: { value } }) => {
              const hour = Math.floor(value / 60);
              const min = Math.floor(value % 60);
              return (
                <>
                  {hour > 0 ? `${hour} hr${hour > 1 ? "s" : ""} ` : ""}
                  {min > 0 ? `${min} min${min > 1 ? "s" : ""}` : ""}
                </>
              );
            }
          },
          {
            Header: "Status",
            accessor: "show.status"
          }
        ]
      }
    ],
    []
  );

  ...
}

...
Enter fullscreen mode Exit fullscreen mode

在这个例子中,我们通过该方法访问值Cell,然后返回计算值或自定义组件。

对于“运行时长”,我们计算小时数并返回自定义值。对于“类型”,我们循环并将该值传递给一个自定义组件,该组件会创建一个类似徽章的元素。

使用 react-table 自定义外观非常简单。完成此步骤后,我们的表格 UI 将如下所示:

表格 UI 中“类型”列带有徽章

这样,我们可以根据需要自定义每个单元格的样式。您可以根据数据值显示每个单元格的任何自定义元素。

添加搜索功能

让我们给表格添加一些功能。如果你查看react-table 的演示页面useFilters,你会发现他们已经提供了创建自定义智能表格所需的一切。他们的演示中唯一缺少的是全局搜索功能。所以我决定使用react-table 的 Hook 插件来实现这个功能。

首先,让我们创建一个搜索输入框Table.js

// Table.js

// Create a state
const [filterInput, setFilterInput] = useState("");

// Update the state when input changes
const handleFilterChange = e => {
  const value = e.target.value || undefined;
  setFilterInput(value);
};

// Input element
<input
  value={filterInput}
  onChange={handleFilterChange}
  placeholder={"Search name"}
/>
Enter fullscreen mode Exit fullscreen mode

管理输入状态非常简单直接。但是现在,如何将此筛选值传递给我们的表格并筛选表格行呢?

为此,react-table 提供了一个不错的 Hook 插件,名为useFilters

// Table.js

const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
    setFilter // The useFilter Hook provides a way to set the filter
  } = useTable(
    {
      columns,
      data
    },
    useFilters // Adding the useFilters Hook to the table
    // You can add as many Hooks as you want. Check the documentation for details. You can even add custom Hooks for react-table here
  );
Enter fullscreen mode Exit fullscreen mode

在我们的示例中,我们将仅针对该Name列设置筛选器。为了在输入值更改时筛选名称,我们需要将第一个参数设置为列访问器或 ID 值,将第二个参数设置为搜索筛选值。

让我们更新一下handleFilterChange函数:

const handleFilterChange = e => {
  const value = e.target.value || undefined;
  setFilter("show.name", value); // Update the show.name filter. Now our table will filter and show only the rows which have a matching value
  setFilterInput(value);
};
Enter fullscreen mode Exit fullscreen mode

这是实现搜索功能后的用户界面:

带有搜索功能的表格用户界面

这是一个非常基础的筛选器示例,react-table API 提供了多种筛选选项。您可以点击此处查看 API 文档。

为表格添加排序功能

让我们为表格实现另一个基本功能:排序。允许对所有列进行排序。这很简单——和筛选一样。我们需要添加一个名为 `sorting` 的插件钩子useSortBy,并创建样式以在表格中显示排序图标。它会自动处理升序/降序排序。

// Table.js

const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
    setFilter
  } = useTable(
    {
      columns,
      data
    },
    useFilters,
    useSortBy // This plugin Hook will help to sort our table columns
  );

// Table header styling and props to allow sorting

<th
  {...column.getHeaderProps(column.getSortByToggleProps())}
  className={
    column.isSorted
      ? column.isSortedDesc
        ? "sort-desc"
        : "sort-asc"
      : ""
  }
>
  {column.render("Header")}
</th>
Enter fullscreen mode Exit fullscreen mode

根据排序结果,我们添加类名sort-descsort-asc。我们还将排序属性添加到列标题中。

{...column.getHeaderProps(column.getSortByToggleProps())}
Enter fullscreen mode Exit fullscreen mode

这将自动启用所有列的排序功能。您可以通过disableSortBy在列上设置选项来禁用特定列的排序。在我们的示例中,我们启用了所有列的排序功能。您可以尝试运行演示程序。

这是我们实现排序功能后的用户界面:

带排序功能的表格用户界面

当然,你还可以进一步扩展这个演示——如果你需要帮助,请在评论区留言。一些扩展思路包括:

  • 使用全局筛选器筛选多列。提示:使用`\t`setAllFilters代替 `\t` setFilter
  • 创建分页并加载更多数据到表格中。
  • 仅允许对特定字段进行排序(禁用sortby对列的排序)
  • 不要将硬编码的搜索值传递给 TV Maze API,而是创建一个输入来直接搜索 TV Maze API(即,移除客户端过滤,并通过 API 添加服务器端电视节目搜索并更改数据)。

可以查看 react-table 的丰富示例页面来扩展这个演示。他们提供了非常全面的示例,几乎涵盖了所有用例,并能提供相应的解决方案。

结论

这就是添加排序功能后的最终演示效果。您可以在这里体验演示并查看其代码库

我们已经学习了如何使用 React 构建表格 UI。创建表格来满足基本需求并不难,但请尽可能避免重复造轮子。希望你喜欢学习表格 UI 的相关知识——欢迎在评论区分享你使用表格的经验。


编者按:发现本文有误?您可以在这里找到正确版本。

插件:LogRocket,一款用于 Web 应用的 DVR

 
LogRocket 控制面板免费试用横幅
 
LogRocket是一款前端日志工具,可让您重现问题,如同在您自己的浏览器中发生一样。无需猜测错误原因,也无需用户提供屏幕截图和日志转储,LogRocket 即可让您重现会话,快速了解问题所在。它与任何框架的应用程序完美兼容,并提供插件来记录来自 Redux、Vuex 和 @ngrx/store 的额外上下文信息。
 
除了记录 Redux 操作和状态之外,LogRocket 还会记录控制台日志、JavaScript 错误、堆栈跟踪、包含标头和正文的网络请求/响应、浏览器元数据以及自定义日志。它还会对 DOM 进行插桩,记录页面上的 HTML 和 CSS,即使是最复杂的单页应用程序,也能生成像素级精确的视频。
 
免费试用


这篇文章《在 React 中构建智能数据表的完整指南》最初发表于LogRocket 博客

文章来源:https://dev.to/bnevilleoneill/the-complete-guide-to-building-a-smart-data-table-in-react-538p