Skip to content

React 服务端数据

dataMode: 'server' + pagination + loadData:表头点排序、列头筛选、翻页 / 切换每页条数全部交给服务端。表格不再本地排序 / 筛选 / 切片,只把状态收进一次请求(req.sort / req.filters / req.page),并处理竞态(连点筛选只采用最后一次响应)。

示例用 400 条本地数据模拟服务端查询,下方日志显示每次实际下发的请求参数。

使用的 API

ts
type Options = {
  list: T[];  // 初始值(远程模式下只作首屏用)
  itemKey: string;  // 行唯一标识字段名
  estimatedSize: number;  // 行预估高度(px)
  // 等价于 manualSorting + manualFiltering + manualPagination
  dataMode: 'server';
  sortMode?: 'single' | 'multiple';
  pagination: { total: number; pageSize: number; pageSizes?: number[] };
  loadData: (req: DataRequest) => Promise<DataResponse<T>>;
  onRemoteStateChange?: (state: RemoteState) => void;
}

// 实例方法(ref 上调用)
tableRef.current?.refresh();  // 保留滚动位置重拉
tableRef.current?.reload();   // 回到第 1 页

注意

loadData 由封装内部包一层读最新 props,所以不需要 useCallback:函数身份变化既不会重建表格也不会触发重新取数。

完整语义(DataRequest 字段、竞态与失败重试、与 manualPagination 的区别)见 Vanilla · 服务端数据

示例

微应用尚未挂载。

源码

点击查看源码
tsx
import React from 'react';
import {
  VirtTableReact,
  type ReactTableColumn,
  type VirtTableRef,
  type DataRequest,
  type DataResponse,
  type RemoteState,
  vtColumnFilter,
} from '@virt-table/react';
import { faker } from '@faker-js/faker';

/**
 * 服务端数据示例:`dataMode: 'server'` + `pagination` + `loadData`。
 *
 * 表头点排序、列头筛选、翻页/切换每页条数**全部交给服务端**:表格不再本地
 * 排序/筛选/切片,只把状态收进一次请求(`req.sort` / `req.filters` / `req.page`),
 * 并处理竞态(连点筛选只采用最后一次响应)。
 *
 * 这里用 400 条本地数据模拟服务端查询;下方日志显示每次实际下发的请求参数。
 */
interface Row extends Record<string, unknown> {
  id: number;
  name: string;
  dept: string;
  qty: number;
  price: number;
}

const depts = ['工程部', '设计部', '市场部', '财务部'];

const columns: ReactTableColumn<Row>[] = [
  { key: 'id', title: 'ID', width: 80, sortable: true },
  { key: 'name', title: '姓名', width: 200, filterType: 'text' },
  {
    key: 'dept',
    title: '部门',
    width: 160,
    filters: depts.map((d) => ({ label: d, value: d })),
    filterMultiple: true,
  },
  { key: 'qty', title: '数量', width: 130, align: 'right', sortable: true },
  { key: 'price', title: '单价', width: 130, align: 'right', sortable: true },
];

// 模拟服务端数据(真实场景中在后端)。懒生成:App 会静态 import 全部示例模块
const TOTAL = 400;
let _db: Row[] | null = null;
const getDb = (): Row[] =>
  (_db ??= Array.from({ length: TOTAL }, (_, i) => ({
    id: i + 1,
    name: faker.person.fullName(),
    dept: depts[i % depts.length]!,
    qty: faker.number.int({ min: 1, max: 20 }),
    price: faker.number.int({ min: 5, max: 500 }),
  })));

const initialPageSize = 20;

export default function ServerDataTable() {
  const tableRef = React.useRef<VirtTableRef>(null);
  const [stat, setStat] = React.useState('');
  const [logs, setLogs] = React.useState<string[]>([]);
  const seqRef = React.useRef(0);

  const log = (msg: string): void => setLogs((prev) => [msg, ...prev].slice(0, 8));

  /** 模拟服务端查询:筛选 → 排序 → 分页切片 */
  const loadData = (req: DataRequest): Promise<DataResponse<Row>> => {
    const seq = ++seqRef.current;
    const sortDesc = req.sort.map((s) => `${s.colKey} ${s.order}`).join(', ') || '-';
    const filterDesc = Object.keys(req.filters).length ? JSON.stringify(req.filters) : '-';
    log(
      `#${seq} ${req.reason} page=${req.page} size=${req.pageSize} sort=[${sortDesc}] filters=${filterDesc}`,
    );

    return new Promise((resolve) => {
      setTimeout(() => {
        let rows = getDb();

        // 筛选(服务端语义:部门多选取并集,姓名关键字模糊匹配)
        for (const [key, values] of Object.entries(req.filters)) {
          if (!values.length) continue;
          if (key === 'name') {
            const kw = String(values[0]).toLowerCase();
            rows = rows.filter((r) => r.name.toLowerCase().includes(kw));
          } else {
            rows = rows.filter((r) => values.includes(r[key] as never));
          }
        }

        // 排序(多列:按 sort 数组顺序依次比较)
        if (req.sort.length) {
          rows = rows.slice().sort((a, b) => {
            for (const { colKey, order } of req.sort) {
              const av = a[colKey];
              const bv = b[colKey];
              if (av === bv) continue;
              const r =
                typeof av === 'number' && typeof bv === 'number'
                  ? av - bv
                  : String(av) < String(bv)
                    ? -1
                    : 1;
              return order === 'desc' ? -r : r;
            }
            return 0;
          });
        }

        const total = rows.length;
        const page = rows.slice(req.offset, req.offset + req.pageSize);
        log(`#${seq} ✓ 返回 ${page.length} 条 / 命中 ${total} 条`);
        resolve({ rows: page, total });
      }, 350);
    });
  };

  return (
    <div className="demo-wrapper">
      <h3 className="demo-title">React 服务端数据</h3>
      <div className="demo-hint">
        服务端模式:排序、筛选、翻页都通过一次请求完成(表格不做本地排序/筛选/切片)。
        切排序或筛选会自动回到第 1 页重新取数。
      </div>
      <div className="virt-table-controls">
        <button type="button" onClick={() => tableRef.current?.refresh()}>
          refresh()(保留滚动位置重拉)
        </button>
        <button type="button" onClick={() => tableRef.current?.reload()}>
          reload()(回到第 1 页)
        </button>
        <span className="demo-note">{stat}</span>
      </div>
      <div style={{ width: 800, height: 460 }} className="demo-container">
        <VirtTableReact
          ref={tableRef}
          columns={columns}
          options={{
            plugins: [vtColumnFilter()],
            list: [],
            itemKey: 'id',
            estimatedSize: 40,
            buffer: 6,
            border: true,
            // 排序 / 筛选 / 分页全部交给服务端(等价于 manualSorting + manualFiltering + manualPagination)
            dataMode: 'server',
            sortMode: 'multiple',
            pagination: {
              total: 0,
              pageSize: initialPageSize,
              pageSizes: [10, 20, 50],
            },
            loadData,
            onRemoteStateChange: (st: RemoteState) =>
              setStat(`第 ${st.page} 页 · 共 ${st.total} 条${st.loading ? ' · 请求中' : ''}`),
          }}
        />
      </div>
      <div
        className="status-text"
        style={{ marginTop: 12, whiteSpace: 'pre-wrap', fontFamily: 'ui-monospace, Menlo, monospace' }}
      >
        {logs.slice(0, 6).join('\n')}
      </div>
    </div>
  );
}