T-CREATOR

Lodash で管理画面テーブルを強化:並び替え・フィルタ・ページングの骨格

Lodash で管理画面テーブルを強化:並び替え・フィルタ・ページングの骨格

管理画面のテーブル機能は、ユーザーが大量のデータを効率的に管理するための重要な要素です。並び替え・フィルタ・ページングといった機能を実装することで、データの見やすさと操作性が大きく向上します。

本記事では、Lodash を活用して、これら 3 つの機能を簡潔かつ効率的に実装する方法を解説していきます。Lodash の便利なメソッドを使えば、複雑なロジックもシンプルに書けるため、コードの保守性も高まりますよ。

背景

管理画面テーブルに求められる機能

現代の Web アプリケーションにおいて、管理画面は業務効率を左右する重要な画面です。特にテーブル表示では、以下のような機能が標準的に求められます。

#機能目的ユーザーメリット
1並び替えデータを昇順・降順で整列目的のデータを素早く発見
2フィルタ条件に合うデータのみ表示ノイズを排除して必要な情報に集中
3ページングデータを分割して表示大量データでも快適に閲覧

これらの機能を組み合わせることで、数千・数万件のデータでもストレスなく操作できる管理画面が実現できます。

Lodash がテーブル実装に適している理由

Lodash は JavaScript のユーティリティライブラリで、配列やオブジェクト操作を簡潔に記述できます。テーブル機能の実装において、以下のような強みがあります。

  • 豊富な配列操作メソッド: orderByfilterchunk などが標準装備
  • チェーン記法: 複数の操作を見通しよく連結可能
  • パフォーマンス: 最適化された内部実装で高速動作
  • 型安全性: TypeScript との相性も良好

下記の図は、Lodash を用いたテーブルデータの処理フローを示しています。

mermaidflowchart LR
  raw["元データ<br/>(配列)"] --> filter["フィルタ<br/>_.filter()"]
  filter --> sort["並び替え<br/>_.orderBy()"]
  sort --> page["ページング<br/>_.chunk()"]
  page --> display["画面表示<br/>(テーブル)"]

このように、元データに対して順次処理を適用することで、最終的な表示データを得る流れになります。

課題

素の JavaScript でテーブル機能を実装する場合の問題

Lodash を使わずに素の JavaScript でテーブル機能を実装すると、以下のような課題に直面します。

並び替えロジックの複雑化

Array.prototype.sort() は破壊的メソッドであり、元の配列を変更してしまいます。また、複数カラムでのソートや、文字列・数値・日付の型ごとの比較関数を自作する必要があり、コードが冗長になりがちです。

フィルタ条件の管理が煩雑

複数の条件を組み合わせたフィルタ処理を実装する際、条件ごとに if 文を積み重ねると可読性が低下します。特に動的にフィルタ条件を追加・削除する場合、ロジックが複雑になりやすいです。

ページング処理の手動実装

データを指定件数ごとに分割し、現在のページ番号に応じて表示範囲を計算する処理を手書きすると、オフセット計算のバグが混入しやすくなります。

以下の図は、素の JavaScript で実装した場合の課題を整理したものです。

mermaidflowchart TB
  subgraph plain["素の JavaScript 実装"]
    p1["破壊的メソッド<br/>(元配列が変更される)"]
    p2["型ごとの比較関数<br/>(自作が必要)"]
    p3["複雑な条件分岐<br/>(if 文の連続)"]
    p4["オフセット計算<br/>(バグ混入リスク)"]
  end

  subgraph issues["課題"]
    i1["保守性の低下"]
    i2["バグ発生率の上昇"]
    i3["開発コストの増加"]
  end

  p1 --> i1
  p2 --> i1
  p3 --> i2
  p4 --> i2
  i1 --> i3
  i2 --> i3

これらの課題を解決するために、Lodash の強力なメソッド群を活用することが有効です。

解決策

Lodash を活用した実装アプローチ

Lodash を使うことで、並び替え・フィルタ・ページングの各機能を簡潔かつ安全に実装できます。以下、それぞれの機能について具体的な実装パターンを見ていきましょう。

並び替え機能の実装

Lodash の orderBy メソッドを使えば、複数カラムでの並び替えや昇順・降順の指定が一行で完結します。

基本的な使い方

typescriptimport _ from 'lodash';

// ユーザーデータの型定義
interface User {
  id: number;
  name: string;
  age: number;
  createdAt: Date;
}

上記では、テーブルに表示するユーザーデータの型を定義しています。idnameagecreatedAt の 4 つのプロパティを持つオブジェクトです。

typescript// サンプルデータ
const users: User[] = [
  {
    id: 1,
    name: '田中太郎',
    age: 28,
    createdAt: new Date('2023-01-15'),
  },
  {
    id: 2,
    name: '佐藤花子',
    age: 34,
    createdAt: new Date('2023-02-20'),
  },
  {
    id: 3,
    name: '鈴木一郎',
    age: 28,
    createdAt: new Date('2023-01-10'),
  },
  {
    id: 4,
    name: '高橋次郎',
    age: 42,
    createdAt: new Date('2023-03-05'),
  },
];

このサンプルデータを使って、並び替えの実装を試していきます。

単一カラムでの並び替え

typescript// 名前で昇順に並び替え
const sortedByName = _.orderBy(users, ['name'], ['asc']);

console.log(sortedByName);
// 結果: 佐藤花子 → 鈴木一郎 → 高橋次郎 → 田中太郎

orderBy の第 2 引数に並び替えるキー(ここでは name)を配列で指定し、第 3 引数に昇順(asc)か降順(desc)かを指定します。

複数カラムでの並び替え

typescript// 年齢の昇順、同じ年齢なら作成日の降順
const sortedMultiple = _.orderBy(
  users,
  ['age', 'createdAt'],
  ['asc', 'desc']
);

console.log(sortedMultiple);
// 結果: 田中太郎(28, 2023-01-15) → 鈴木一郎(28, 2023-01-10) → 佐藤花子(34) → 高橋次郎(42)

複数のキーを配列で指定することで、優先順位付きの並び替えが簡単に実現できます。第 1 ソートキーで同じ値の場合、第 2 ソートキーが適用される仕組みです。

フィルタ機能の実装

Lodash の filter メソッドを使うと、条件関数を簡潔に記述できます。

基本的なフィルタ

typescript// 30歳以上のユーザーを抽出
const filteredByAge = _.filter(
  users,
  (user) => user.age >= 30
);

console.log(filteredByAge);
// 結果: 佐藤花子(34)、高橋次郎(42)

条件関数を渡すだけで、該当するデータのみを抽出できます。この例では、age が 30 以上のユーザーのみが返されます。

複数条件のフィルタ

typescript// 28歳以上かつ2023年1月以降に作成されたユーザー
const filteredMultiple = _.filter(users, (user) => {
  return (
    user.age >= 28 &&
    user.createdAt >= new Date('2023-01-01')
  );
});

console.log(filteredMultiple);
// 結果: 田中太郎、佐藤花子、鈴木一郎、高橋次郎

複数の条件を &&|| で組み合わせることで、柔軟なフィルタリングが可能です。

文字列検索フィルタ

typescript// 名前に「太郎」を含むユーザーを検索
const searchByName = _.filter(users, (user) => {
  return user.name.includes('太郎');
});

console.log(searchByName);
// 結果: 田中太郎

includes メソッドを使えば、部分一致検索も簡単に実装できます。管理画面でよくある検索ボックスの機能に最適ですね。

ページング機能の実装

Lodash の chunk メソッドを使うと、配列を指定サイズごとに分割できます。

基本的なページング

typescript// 1ページあたり2件でデータを分割
const pageSize = 2;
const paginatedData = _.chunk(users, pageSize);

console.log(paginatedData);
// 結果:
// [
//   [田中太郎, 佐藤花子],
//   [鈴木一郎, 高橋次郎]
// ]

chunk は配列を指定した件数ごとに分割し、二次元配列として返します。各内部配列が 1 ページ分のデータに相当します。

特定ページのデータ取得

typescript// 2ページ目のデータを取得(0ベース)
const currentPage = 1; // 2ページ目
const pageData = paginatedData[currentPage];

console.log(pageData);
// 結果: [鈴木一郎, 高橋次郎]

ページ番号をインデックスとして指定するだけで、該当ページのデータを取得できます。UI 側でページネーションボタンをクリックした際に、このロジックを呼び出せば良いわけです。

総ページ数の計算

typescript// 総ページ数を算出
const totalPages = paginatedData.length;

console.log(`総ページ数: ${totalPages}`);
// 結果: 総ページ数: 2

chunk で分割した配列の長さが、そのまま総ページ数になります。ページネーションコンポーネントの表示に使えますね。

3 つの機能を組み合わせる

実際の管理画面では、並び替え・フィルタ・ページングを組み合わせて使います。以下、統合的な実装例を見てみましょう。

テーブル処理関数の作成

typescriptinterface TableParams {
  data: User[];
  filterCondition?: (user: User) => boolean;
  sortKeys?: string[];
  sortOrders?: ('asc' | 'desc')[];
  page?: number;
  pageSize?: number;
}

テーブル処理に必要なパラメータを一つのインターフェースで定義します。オプショナルパラメータにすることで、必要な機能のみを指定できる柔軟性を持たせています。

typescriptfunction processTableData(params: TableParams) {
  const {
    data,
    filterCondition,
    sortKeys = ['id'],
    sortOrders = ['asc'],
    page = 0,
    pageSize = 10,
  } = params;

  // 1. フィルタ処理
  let processedData = filterCondition
    ? _.filter(data, filterCondition)
    : data;

  // 2. 並び替え処理
  processedData = _.orderBy(
    processedData,
    sortKeys,
    sortOrders
  );

  // 3. ページング処理
  const chunkedData = _.chunk(processedData, pageSize);
  const pageData = chunkedData[page] || [];

  return {
    pageData,
    totalCount: processedData.length,
    totalPages: chunkedData.length,
    currentPage: page,
  };
}

この関数は、フィルタ → 並び替え → ページングの順で処理を実行します。順序が重要で、まずフィルタで絞り込み、次に並び替え、最後にページングを行うことで正しい結果が得られます。

実際の使用例

typescript// 30歳以上のユーザーを年齢の降順で並び替え、1ページ目を表示
const result = processTableData({
  data: users,
  filterCondition: (user) => user.age >= 30,
  sortKeys: ['age'],
  sortOrders: ['desc'],
  page: 0,
  pageSize: 2,
});

console.log(result);
// 結果:
// {
//   pageData: [高橋次郎(42), 佐藤花子(34)],
//   totalCount: 2,
//   totalPages: 1,
//   currentPage: 0
// }

このように、一つの関数呼び出しで複雑なテーブル処理を実行できます。返り値には表示用データだけでなく、総件数や総ページ数も含まれるため、UI コンポーネント側での表示が容易になります。

下記の図は、3 つの機能を組み合わせた処理フローを示しています。

mermaidflowchart TB
  start["元データ<br/>(users配列)"] --> filter_check{"フィルタ条件<br/>あり?"}
  filter_check -->|Yes| filter_apply["_.filter()で絞り込み"]
  filter_check -->|No| sort_apply["_.orderBy()で並び替え"]
  filter_apply --> sort_apply
  sort_apply --> chunk["_.chunk()でページ分割"]
  chunk --> extract["指定ページのデータ抽出"]
  extract --> result["結果を返す<br/>(pageData, totalCount, etc.)"]

このフローに従うことで、どの機能も独立して動作しつつ、組み合わせた際にも矛盾なく処理できる設計になっています。

具体例

React コンポーネントでの実装例

ここからは、実際の React コンポーネントで Lodash を使ったテーブル機能を実装する例を見ていきます。

基本的なテーブルコンポーネント

typescriptimport React, { useState, useMemo } from 'react';
import _ from 'lodash';

interface User {
  id: number;
  name: string;
  age: number;
  createdAt: Date;
}

まず、必要なモジュールと型定義をインポートします。useMemo はパフォーマンス最適化のために使用します。

typescriptconst UserTable: React.FC = () => {
  // サンプルデータ
  const [users] = useState<User[]>([
    { id: 1, name: '田中太郎', age: 28, createdAt: new Date('2023-01-15') },
    { id: 2, name: '佐藤花子', age: 34, createdAt: new Date('2023-02-20') },
    { id: 3, name: '鈴木一郎', age: 28, createdAt: new Date('2023-01-10') },
    { id: 4, name: '高橋次郎', age: 42, createdAt: new Date('2023-03-05') },
    { id: 5, name: '伊藤美咲', age: 25, createdAt: new Date('2023-04-12') },
    { id: 6, name: '渡辺健', age: 38, createdAt: new Date('2023-05-08') }
  ]);

  // 状態管理
  const [searchName, setSearchName] = useState('');
  const [minAge, setMinAge] = useState<number | undefined>();
  const [sortKey, setSortKey] = useState<keyof User>('id');
  const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
  const [currentPage, setCurrentPage] = useState(0);
  const pageSize = 3;

コンポーネントの状態を定義します。検索キーワード、年齢フィルタ、並び替えキー、並び順、現在ページなど、ユーザー操作に応じて変化する値を管理します。

データ処理ロジック

typescript// テーブルデータの処理(useMemoで最適化)
const tableData = useMemo(() => {
  // 1. フィルタ処理
  let filtered = _.filter(users, (user) => {
    const nameMatch = user.name.includes(searchName);
    const ageMatch = minAge ? user.age >= minAge : true;
    return nameMatch && ageMatch;
  });

  // 2. 並び替え処理
  filtered = _.orderBy(filtered, [sortKey], [sortOrder]);

  // 3. ページング処理
  const chunked = _.chunk(filtered, pageSize);
  const pageData = chunked[currentPage] || [];

  return {
    pageData,
    totalCount: filtered.length,
    totalPages: chunked.length,
  };
}, [
  users,
  searchName,
  minAge,
  sortKey,
  sortOrder,
  currentPage,
]);

useMemo を使うことで、依存する状態が変更されたときのみ再計算を行います。これにより、不要な再計算を防いでパフォーマンスを向上させています。

UI 部分の実装

typescript  return (
    <div>
      {/* 検索・フィルタUI */}
      <div style={{ marginBottom: '20px' }}>
        <input
          type="text"
          placeholder="名前で検索"
          value={searchName}
          onChange={(e) => {
            setSearchName(e.target.value);
            setCurrentPage(0); // 検索時はページをリセット
          }}
          style={{ marginRight: '10px' }}
        />
        <input
          type="number"
          placeholder="最低年齢"
          value={minAge || ''}
          onChange={(e) => {
            setMinAge(e.target.value ? Number(e.target.value) : undefined);
            setCurrentPage(0); // フィルタ時はページをリセット
          }}
          style={{ marginRight: '10px' }}
        />
      </div>

検索ボックスと年齢フィルタの入力欄を配置します。値が変更されたときに currentPage を 0 にリセットすることで、フィルタ後は常に 1 ページ目から表示されるようにしています。

typescript      {/* テーブル本体 */}
      <table border={1} style={{ width: '100%', borderCollapse: 'collapse' }}>
        <thead>
          <tr>
            <th
              onClick={() => {
                setSortKey('id');
                setSortOrder(sortKey === 'id' && sortOrder === 'asc' ? 'desc' : 'asc');
              }}
              style={{ cursor: 'pointer' }}
            >
              ID {sortKey === 'id' && (sortOrder === 'asc' ? '↑' : '↓')}
            </th>
            <th
              onClick={() => {
                setSortKey('name');
                setSortOrder(sortKey === 'name' && sortOrder === 'asc' ? 'desc' : 'asc');
              }}
              style={{ cursor: 'pointer' }}
            >
              名前 {sortKey === 'name' && (sortOrder === 'asc' ? '↑' : '↓')}
            </th>
            <th
              onClick={() => {
                setSortKey('age');
                setSortOrder(sortKey === 'age' && sortOrder === 'asc' ? 'desc' : 'asc');
              }}
              style={{ cursor: 'pointer' }}
            >
              年齢 {sortKey === 'age' && (sortOrder === 'asc' ? '↑' : '↓')}
            </th>
            <th
              onClick={() => {
                setSortKey('createdAt');
                setSortOrder(sortKey === 'createdAt' && sortOrder === 'asc' ? 'desc' : 'asc');
              }}
              style={{ cursor: 'pointer' }}
            >
              作成日 {sortKey === 'createdAt' && (sortOrder === 'asc' ? '↑' : '↓')}
            </th>
          </tr>
        </thead>

テーブルのヘッダー行です。各カラムをクリックすると、そのカラムでの並び替えがトグルされます。現在のソートキーと順序を矢印アイコンで視覚的に示しています。

typescript        <tbody>
          {tableData.pageData.map((user) => (
            <tr key={user.id}>
              <td>{user.id}</td>
              <td>{user.name}</td>
              <td>{user.age}</td>
              <td>{user.createdAt.toLocaleDateString('ja-JP')}</td>
            </tr>
          ))}
        </tbody>
      </table>

現在のページに表示するデータを map で展開し、各行を描画します。key には一意の id を指定することで、React の差分検出を効率化しています。

ページネーション部分

typescript      {/* ページネーション */}
      <div style={{ marginTop: '20px' }}>
        <button
          onClick={() => setCurrentPage(Math.max(0, currentPage - 1))}
          disabled={currentPage === 0}
        >
          前へ
        </button>
        <span style={{ margin: '0 10px' }}>
          {currentPage + 1} / {tableData.totalPages || 1} ページ
          (全 {tableData.totalCount} 件)
        </span>
        <button
          onClick={() => setCurrentPage(Math.min(tableData.totalPages - 1, currentPage + 1))}
          disabled={currentPage >= tableData.totalPages - 1}
        >
          次へ
        </button>
      </div>
    </div>
  );
};

export default UserTable;

ページネーションボタンと現在の状態を表示します。disabled 属性を使って、最初のページでは「前へ」ボタン、最後のページでは「次へ」ボタンを無効化し、ユーザーに範囲外の操作をさせないようにしています。

下記の図は、React コンポーネント内でのデータフローを示しています。

mermaidflowchart TB
  subgraph ui["ユーザーインターフェース"]
    search["検索ボックス"]
    age_filter["年齢フィルタ"]
    header["テーブルヘッダー<br/>(クリックでソート)"]
    pagination["ページネーション"]
  end

  subgraph state["Reactステート"]
    s_search["searchName"]
    s_age["minAge"]
    s_sort["sortKey, sortOrder"]
    s_page["currentPage"]
  end

  subgraph logic["処理ロジック(useMemo)"]
    lodash["Lodash処理<br/>(filter → orderBy → chunk)"]
  end

  subgraph display["表示データ"]
    table["テーブル本体"]
    info["件数・ページ情報"]
  end

  search --> s_search
  age_filter --> s_age
  header --> s_sort
  pagination --> s_page

  s_search --> lodash
  s_age --> lodash
  s_sort --> lodash
  s_page --> lodash

  lodash --> table
  lodash --> info

このように、UI の操作がステートを更新し、ステートの変更が useMemo によって処理され、最終的に表示データが更新される流れになっています。

図で理解できる要点

  • ユーザー操作(検索、フィルタ、ソート、ページ移動)がそれぞれ対応するステートを更新
  • useMemo が依存ステートの変更を検知し、Lodash 処理を再実行
  • 処理結果がテーブル本体とページ情報の表示に反映される
  • 単方向データフローにより、状態管理が明確で予測可能

パフォーマンス最適化のポイント

useMemo の活用

typescript// 悪い例:毎回再計算される
const tableData = processTableData({
  data: users,
  filterCondition: (user) => user.name.includes(searchName),
  sortKeys: [sortKey],
  sortOrders: [sortOrder],
  page: currentPage,
  pageSize: 3,
});

この書き方では、コンポーネントが再レンダリングされるたびにテーブル処理が実行されてしまいます。

typescript// 良い例:依存値が変わったときのみ再計算
const tableData = useMemo(() => {
  return processTableData({
    data: users,
    filterCondition: (user) =>
      user.name.includes(searchName),
    sortKeys: [sortKey],
    sortOrders: [sortOrder],
    page: currentPage,
    pageSize: 3,
  });
}, [users, searchName, sortKey, sortOrder, currentPage]);

useMemo で囲むことで、依存配列の値が変わったときのみ処理が実行されます。特に大量データを扱う場合、この最適化が大きな効果を発揮します。

デバウンス処理の導入

typescriptimport { debounce } from 'lodash';

// 検索入力のデバウンス処理
const debouncedSearch = useMemo(
  () =>
    debounce((value: string) => {
      setSearchName(value);
      setCurrentPage(0);
    }, 300),
  []
);

// 入力ハンドラ
const handleSearchChange = (
  e: React.ChangeEvent<HTMLInputElement>
) => {
  debouncedSearch(e.target.value);
};

検索入力のたびに処理が走るとパフォーマンスが低下するため、Lodash の debounce を使って入力終了後に処理を実行するようにします。300ms 待機してから処理を実行することで、ユーザーの入力中は無駄な処理を抑えられます。

エラーハンドリングとエッジケース

データが空の場合の対応

typescript// テーブルボディ部分の改善
<tbody>
  {tableData.pageData.length > 0 ? (
    tableData.pageData.map((user) => (
      <tr key={user.id}>
        <td>{user.id}</td>
        <td>{user.name}</td>
        <td>{user.age}</td>
        <td>
          {user.createdAt.toLocaleDateString('ja-JP')}
        </td>
      </tr>
    ))
  ) : (
    <tr>
      <td
        colSpan={4}
        style={{ textAlign: 'center', padding: '20px' }}
      >
        該当するデータがありません
      </td>
    </tr>
  )}
</tbody>

フィルタ結果が空の場合、メッセージを表示することでユーザーに状況を明確に伝えます。colSpan を使って全カラムを結合し、中央寄せで表示することで視認性を高めています。

ページ範囲外アクセスの防止

typescript// ページング処理の改善版
const tableData = useMemo(() => {
  // ... フィルタ・ソート処理 ...

  const chunked = _.chunk(filtered, pageSize);

  // ページ番号が範囲外の場合は0にリセット
  const safePage = Math.min(
    Math.max(0, currentPage),
    chunked.length - 1
  );
  const pageData = chunked[safePage] || [];

  return {
    pageData,
    totalCount: filtered.length,
    totalPages: chunked.length,
    currentPage: safePage,
  };
}, [
  users,
  searchName,
  minAge,
  sortKey,
  sortOrder,
  currentPage,
]);

フィルタ条件を変更した際、総ページ数が減って現在のページ番号が範囲外になる可能性があります。Math.minMath.max を使って安全な範囲に収めることで、エラーを防ぎます。

TypeScript での型安全性強化

ジェネリック関数化

typescript// 汎用的なテーブル処理関数
function processTableData<T>(params: {
  data: T[];
  filterCondition?: (item: T) => boolean;
  sortKeys?: (keyof T)[];
  sortOrders?: ('asc' | 'desc')[];
  page?: number;
  pageSize?: number;
}) {
  const {
    data,
    filterCondition,
    sortKeys = [],
    sortOrders = [],
    page = 0,
    pageSize = 10,
  } = params;

  let processed = filterCondition
    ? _.filter(data, filterCondition)
    : data;

  if (sortKeys.length > 0) {
    processed = _.orderBy(
      processed,
      sortKeys as string[],
      sortOrders
    );
  }

  const chunked = _.chunk(processed, pageSize);
  const pageData = chunked[page] || [];

  return {
    pageData,
    totalCount: processed.length,
    totalPages: chunked.length,
    currentPage: page,
  };
}

ジェネリック型 <T> を使うことで、あらゆるデータ型に対応できる汎用的なテーブル処理関数になります。型パラメータにより、sortKeysT のキーのみを受け付けるようになり、タイプセーフになりますよ。

使用例

typescript// User型で使用
const userResult = processTableData<User>({
  data: users,
  filterCondition: (user) => user.age >= 30,
  sortKeys: ['age', 'name'], // User型のキーのみ指定可能
  sortOrders: ['desc', 'asc'],
  page: 0,
  pageSize: 5,
});

// Product型でも同じ関数を使用可能
interface Product {
  id: number;
  name: string;
  price: number;
}

const productResult = processTableData<Product>({
  data: products,
  sortKeys: ['price'], // Product型のキーのみ指定可能
  page: 0,
  pageSize: 10,
});

同じ関数を異なるデータ型に対して安全に使い回せるため、コードの重複を減らせます。

まとめ

本記事では、Lodash を活用した管理画面テーブルの並び替え・フィルタ・ページング機能の実装方法を解説しました。重要なポイントを振り返ってみましょう。

実装のポイント

#機能Lodash メソッド主な利点
1並び替え_.orderBy()複数カラム対応、型を問わない比較
2フィルタ_.filter()条件関数を簡潔に記述
3ページング_.chunk()配列分割を一行で実装
4デバウンス_.debounce()入力処理の最適化

開発時の注意点

  • 処理順序の厳守: フィルタ → 並び替え → ページングの順で処理することで正しい結果を得られます
  • useMemo による最適化: 依存値が変わったときのみ再計算することでパフォーマンスを向上させます
  • エッジケースへの対応: 空データやページ範囲外アクセスなど、例外的なケースにも対処しましょう
  • 型安全性の確保: TypeScript のジェネリクスを活用し、コンパイル時にエラーを検出できるようにします
  • ユーザビリティの配慮: フィルタ変更時のページリセット、ソート方向の視覚的表示など、使いやすさを意識します

今後の発展

本記事で紹介した基本的な実装をベースに、以下のような機能追加も検討できます。

  • カラムの表示/非表示切り替え: ユーザーが必要なカラムのみを表示できる機能
  • 複数カラムでの同時ソート: Shift キーを押しながらクリックで複数カラムのソート条件を追加
  • カスタムフィルタ UI: 日付範囲指定、プルダウンでの選択など、より高度なフィルタ条件
  • CSV エクスポート: 現在のフィルタ・ソート条件でのデータをファイル出力
  • URL パラメータとの同期: ブラウザの URL にフィルタ・ソート・ページ状態を反映し、共有やブックマークを可能に

Lodash の豊富なメソッドを活用することで、これらの拡張も比較的容易に実装できるでしょう。管理画面のテーブル機能は地味ながら重要な要素ですので、ユーザーが快適にデータを扱える仕組みを構築してみてくださいね。

関連リンク