logo

2023422

Reactで検索・ソート可能なDataTableを自作する

最近、MUI の妹分の UI ライブラリである Joy-UI を使ってます。現在進行形で活発に開発が進んでいて、設計(デザイン)も今時な感じで好感触です。ところどころまだ開発されていないコンポーネントもちらほらあるものの、ドキュメントには代替策がコード付きで載っていてとても親切です。

MUI X というより発展的なコンポーネントをもつ UI ライブラリもあるのですが、そこに今回のテーマである「データテーブル」に該当する Data Grid というものがあります。これは超すごくて、雑に言うと Excel がそのまま再現できちゃいそうなコンポーネントです(超雑)。

ただ残念ながら2023年4月現在では Data Grid の Joy-UI との統合は正式にサポートされていないらしいので、今回は簡易的なデータテーブルを自作していきたいと思います。

要件

  • rows には表示するデータである object[] を渡す
  • columns に指定したフィールド名の値が表示される
  • データの検索ができる
  • 特定のカラムの昇順・降順での並び替えができる

できたもの

上の表が素の HTML 要素だけで作ったもの、下の表が Joy-UI を使ったものです。検索やソートのロジックは一緒です。Joy-UI の方はヘッダーの hover 時に矢印が表示されるのでちょっとわかりやすいです。

ポイント

検索(絞り込み)はただ filter かけているだけなので書くことないです。
並び替えの条件のところが少し複雑なのでちょっと解説します。

状態として sortByorder を持っています。sortBy は並び替える対象のカラム名、order は昇順か降順かです。どちらも undefined になりえます(オリジナルの順番を保持する時)。

type Order = "asc" | "desc";
const [sortBy, setSortBy] = useState<string | undefined>();
const [order, setOrder] = useState<Order | undefined>();

ヘッダー部分はこのようになります。ヘッダーのカラム名をクリックすると、未指定(undefined)→降順(desc)→昇順(asc)→未指定の順で切り替わります。

各カラムについて CSS の transformopacity を計算します。下向き矢印のアイコンを使用しているので、order === "asc" の時のみ180度回転します。order の状態は全カラム共通になっています。opacitysortBy === col.field つまり該当のカラムによってソートされている時のみ表示されるようになっています。

onClick 時には sort 関数が呼ばれます。
該当カラムによってソートされている時は順繰りに desc → asc → undefined を切り替えます。次が undefined の時は sortBy も undefined にします。
他のカラムによってソートされている時は現在の order を維持しつつ sortBy だけ該当カラムに移します。どのカラムでもソートされていない時(オリジナルの順番の時)は sortBy を移しつつ、order を undefined → desc にします。

const header = columns.map((col) => {
  const transform = order === "asc" ? "rotate(180deg)" : undefined;
  const opacity = sortBy === col.field ? 1 : 0;
  const sort = () => {
    if (sortBy === col.field) {
      const orderNext =
        order === "desc" ? "asc" : order === "asc" ? undefined : "desc";
      setSortBy(orderNext ? col.field : undefined);
      setOrder(orderNext);
    } else {
      setSortBy(col.field);
      setOrder(order ? order : "desc");
    }
  };
  return (
    <th key={`header-${col.field}`} style={{ width: col.width }}>
      <button
        style={{
          fontWeight: "bold"
        }}
        onClick={sort}
      >
        <span>{col.headerName}</span>
        <ArrowDownIcon
          style={{
            transition: "0.2s",
            transform: transform,
            opacity: opacity
          }}
        />
      </button>
    </th>
  );
});

実際に配列をソートしている処理はこちらです。
Array.prototype.sort() は破壊的に配列を変更するので、注意です。下記では前の処理で filter をかけているので元の配列が変更されていませんが、そうしない場合は [...rows].sort() とするなど、コピーするようにしましょう。

IE11 をサポートすることはもはやないと思いますが、古いブラウザでは sort の安定性が保証されていません。サポートする場合は MUI の Order Dashboard のデモにある stableSort を見てみてください。

const sortedRows = filteredRows.sort((a, b) => {
  if (!sortBy || !order) {
    return 0;
  }
  const aVal = sortBy in a ? a[sortBy] : "";
  const bVal = sortBy in b ? b[sortBy] : "";
  if (order === "asc") {
    return aVal > bVal ? 1 : -1;
  } else {
    return aVal < bVal ? 1 : -1;
  }
});

インターフェイス

DataTable (素の HTML)

type DataTableProps = {
  columns: Column[];
  rows: object[];
  getRowId?: (row: object) => any;
  search?: any;
  style?: StyleHTMLAttributes<HTMLTableElement>;  // tableのstyle属性
};

JoyDataTable (Joy-UI の Table を使用)

type JoyDataTableProps = {
  columns: Column[];
  rows: object[];
  getRowId?: (row: object) => any;
  search?: any;
  sx?: SxProps;  // @mui/joy/TableのSx属性
};
  • column はカラムの定義です
    export type Column = {
      field: string;  // rowsの各objectのフィールド名
      headerName: string;  // ヘッダーに表示するカラム名
      width?: number;  // カラムの幅
    };
  • rows には任意のオブジェクトの配列を渡します
  • getRowId は rows の各 object の一意の識別子を取得する関数です。デフォルトでは id フィールドを取得します
    • DataGrid にも同じ役割のものがあります1
  • search には検索する文字列や数値を渡します

おわりに

成熟した UI ライブラリならこういった DataTable を備えていることが多いと思いますが、開発途上のものだとあるとは限らないので、React に限らず DataTable を実装してみたい方の参考になれば幸いです!

参考文献

Footnotes

  1. Row identifier