T-CREATOR

React で管理画面を最短構築:テーブル・フィルタ・権限制御の実例

React で管理画面を最短構築:テーブル・フィルタ・権限制御の実例

管理画面の開発は、ビジネスアプリケーションにおいて避けて通れない重要な要素です。データの一覧表示、検索、フィルタリング、そして適切な権限管理により、業務効率を大きく向上させることができますね。

しかし、これらの機能を一から実装するのは時間がかかり、保守性の課題も生じがちです。本記事では、React と TypeScript を活用して、実務で使える管理画面を最短で構築する方法を、実例とともにご紹介します。テーブル表示から高度なフィルタ機能、そして権限制御まで、段階的に実装していきましょう。

背景

管理画面に求められる機能

現代の Web アプリケーションにおいて、管理画面は単なるデータ表示ツールではありません。ユーザー管理、商品管理、注文管理など、様々な業務データを効率的に操作できる必要があります。

典型的な管理画面には、以下の機能が求められます。

#機能重要度説明
1データテーブル表示★★★大量データを見やすく整理して表示
2ソート・検索★★★目的のデータを素早く見つける
3フィルタリング★★☆条件に合うデータのみ抽出
4ページネーション★★★パフォーマンスと UX の両立
5権限制御★★★セキュリティとデータ保護

React エコシステムの充実

React のエコシステムは、これらの要件に対応する優れたライブラリが豊富に揃っています。特に TanStack Table(旧 React Table)は、柔軟性と拡張性を兼ね備えた強力なテーブルライブラリとして注目されていますね。

また、TypeScript との組み合わせにより、型安全性を保ちながら開発できるため、大規模なプロジェクトでも保守性を維持できます。

以下の図は、React 管理画面の基本的なアーキテクチャを示しています。

mermaidflowchart TB
  user["管理者ユーザー"] -->|アクセス| auth["認証・権限チェック"]
  auth -->|OK| dashboard["管理画面<br/>Dashboard"]
  auth -->|NG| error["エラー表示"]

  dashboard --> table["テーブル表示<br/>TanStack Table"]
  dashboard --> filter["フィルタ機能<br/>検索/ソート"]
  dashboard --> paging["ページネーション"]

  table --> api["API レイヤー"]
  filter --> api
  paging --> api

  api --> backend["Backend<br/>REST/GraphQL"]
  backend --> db[("Database")]

上記の図から分かるように、認証から始まり、各機能が API レイヤーを通じてバックエンドと通信する構造になっています。この設計により、フロントエンドとバックエンドを疎結合に保てるのです。

課題

従来の管理画面開発の問題点

管理画面を独自実装する場合、いくつかの課題に直面することが多いでしょう。

パフォーマンスの問題

大量のデータを扱う管理画面では、レンダリングパフォーマンスが重要です。数千件のデータを一度に表示しようとすると、ブラウザが固まってしまうこともあります。

状態管理の複雑化

フィルタ条件、ソート順、ページ位置など、管理すべき状態が多岐にわたります。これらを適切に管理しないと、バグの温床になってしまいますね。

権限制御の実装漏れ

権限チェックを各コンポーネントに散在させると、実装漏れや不整合が発生しやすくなります。セキュリティ上、これは致命的な問題です。

以下の図は、従来の実装で発生しがちな課題を示しています。

mermaidflowchart LR
  comp1["コンポーネント A"] -->|独自実装| state1["状態管理 A"]
  comp2["コンポーネント B"] -->|独自実装| state2["状態管理 B"]
  comp3["コンポーネント C"] -->|独自実装| state3["状態管理 C"]

  state1 -.->|重複| prob["問題点"]
  state2 -.->|不整合| prob
  state3 -.->|保守困難| prob

  prob --> issue1["パフォーマンス低下"]
  prob --> issue2["バグ増加"]
  prob --> issue3["開発効率悪化"]

各コンポーネントが個別に状態管理を行うと、重複や不整合が生じやすく、結果として保守コストが増大してしまいます。

開発時間とコストの増加

これらの問題を解決するため、独自に実装を進めると、開発時間が膨らみ、結果的にプロジェクト全体のコストが増加します。特にテーブル機能の実装は、見た目以上に複雑なロジックが必要になるのです。

解決策

TanStack Table による効率的なテーブル実装

TanStack Table は、ヘッドレス UI ライブラリとして設計されており、ロジックと表示を分離できます。これにより、デザインの自由度を保ちながら、強力な機能を簡単に実装できますね。

TanStack Table の主な特徴

#特徴メリット
1ヘッドレス UIデザインの完全な自由度
2TypeScript フルサポート型安全な開発
3ソート・フィルタ内蔵複雑なロジックを簡単に実装
4仮想化対応大量データでも高速表示
5カスタマイズ可能プロジェクト固有の要件に対応

React Context による権限管理

権限制御は、React Context API を活用することで、アプリケーション全体で一元管理できます。各コンポーネントで個別にチェックするのではなく、Context から権限情報を取得する設計にすることで、保守性が大幅に向上するでしょう。

以下の図は、推奨する管理画面のアーキテクチャを示しています。

mermaidflowchart TB
  app["App ルート"] --> authProvider["AuthProvider<br/>権限 Context"]
  authProvider --> layout["Layout コンポーネント"]

  layout --> table["TanStack Table<br/>データ表示"]
  layout --> filter["Filter コンポーネント<br/>検索・絞り込み"]
  layout --> pagination["Pagination<br/>ページング"]

  table --> hook1["useAuth<br/>権限チェック"]
  filter --> hook2["useTableState<br/>状態管理"]
  pagination --> hook2

  hook1 --> context["Shared Context"]
  hook2 --> context

  context --> api["API 呼び出し"]

この設計では、Context による一元管理と、カスタムフックによる状態の再利用により、コードの重複を排除し、保守性を高めています。

カスタムフックによる状態管理の統一

テーブルの状態(フィルタ、ソート、ページ)を管理するカスタムフックを作成することで、複数の画面で同じロジックを再利用できます。これにより、DRY 原則を守りながら、一貫性のある実装が可能になりますね。

具体例

それでは、実際のコードを見ながら、管理画面を構築していきましょう。

プロジェクトのセットアップ

まず、必要なパッケージをインストールします。TanStack Table と型定義を含めた基本的な依存関係を追加しましょう。

typescript// package.json の dependencies に追加
yarn add @tanstack/react-table
yarn add -D @types/react @types/react-dom

データ型定義

TypeScript を使用するため、まず扱うデータの型を定義します。ユーザー管理画面を例に、User 型を作成しましょう。

typescript// types/user.ts

// ユーザーの役割を定義
export type UserRole = 'admin' | 'editor' | 'viewer';

// ユーザーの状態を定義
export type UserStatus = 'active' | 'inactive' | 'pending';

次に、ユーザーデータの型を定義します。

typescript// types/user.ts (続き)

// ユーザーデータの型定義
export interface User {
  id: string;
  name: string;
  email: string;
  role: UserRole;
  status: UserStatus;
  createdAt: Date;
  lastLoginAt?: Date;
}

フィルタ条件の型も定義しておきます。これにより、型安全なフィルタリングが可能になりますね。

typescript// types/filter.ts

import { UserRole, UserStatus } from './user';

// フィルタ条件の型定義
export interface UserFilterParams {
  searchText?: string; // 名前・メールで検索
  role?: UserRole; // 役割でフィルタ
  status?: UserStatus; // 状態でフィルタ
  page: number; // 現在のページ
  pageSize: number; // 1ページあたりの件数
}

認証と権限管理の実装

権限管理を行う Context を作成します。ログイン中のユーザー情報と、権限チェック用の関数を提供しましょう。

typescript// contexts/AuthContext.tsx

import {
  createContext,
  useContext,
  ReactNode,
} from 'react';
import { User, UserRole } from '../types/user';

// Context の型定義
interface AuthContextType {
  currentUser: User | null;
  hasRole: (role: UserRole) => boolean;
  canEdit: () => boolean;
  canDelete: () => boolean;
}

Context の実装を続けます。

typescript// contexts/AuthContext.tsx (続き)

// Context の作成(初期値は undefined)
const AuthContext = createContext<
  AuthContextType | undefined
>(undefined);

// Provider コンポーネントの Props
interface AuthProviderProps {
  children: ReactNode;
  user: User | null; // 認証済みユーザー情報
}

Provider コンポーネントを実装します。ここで権限チェックのロジックを集約することで、アプリケーション全体で一貫した権限管理が可能になります。

typescript// contexts/AuthContext.tsx (続き)

export const AuthProvider = ({
  children,
  user,
}: AuthProviderProps) => {
  // 特定の役割を持っているかチェック
  const hasRole = (role: UserRole): boolean => {
    if (!user) return false;

    // admin は全ての権限を持つ
    if (user.role === 'admin') return true;

    return user.role === role;
  };

  // 編集権限のチェック(admin と editor のみ)
  const canEdit = (): boolean => {
    return hasRole('admin') || hasRole('editor');
  };

  // 削除権限のチェック(admin のみ)
  const canDelete = (): boolean => {
    return hasRole('admin');
  };

  const value = {
    currentUser: user,
    hasRole,
    canEdit,
    canDelete,
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
};

Context を使いやすくするためのカスタムフックも作成します。

typescript// contexts/AuthContext.tsx (続き)

// カスタムフック:認証情報へのアクセス
export const useAuth = () => {
  const context = useContext(AuthContext);

  if (context === undefined) {
    throw new Error(
      'useAuth must be used within an AuthProvider'
    );
  }

  return context;
};

テーブルコンポーネントの実装

TanStack Table を使用したテーブルコンポーネントを作成します。まず、カラム定義から始めましょう。

typescript// components/UserTable/columns.tsx

import { ColumnDef } from '@tanstack/react-table';
import { User } from '../../types/user';

// ステータスバッジコンポーネント
const StatusBadge = ({
  status,
}: {
  status: User['status'];
}) => {
  const colorMap = {
    active: 'bg-green-100 text-green-800',
    inactive: 'bg-gray-100 text-gray-800',
    pending: 'bg-yellow-100 text-yellow-800',
  };

  return (
    <span
      className={`px-2 py-1 rounded ${colorMap[status]}`}
    >
      {status}
    </span>
  );
};

カラム定義を作成します。各カラムのレンダリング方法を指定できますね。

typescript// components/UserTable/columns.tsx (続き)

// テーブルのカラム定義
export const userColumns: ColumnDef<User>[] = [
  {
    accessorKey: 'name',
    header: '名前',
    cell: (info) => info.getValue(),
  },
  {
    accessorKey: 'email',
    header: 'メールアドレス',
    cell: (info) => info.getValue(),
  },
  {
    accessorKey: 'role',
    header: '役割',
    cell: (info) => {
      const roleMap = {
        admin: '管理者',
        editor: '編集者',
        viewer: '閲覧者',
      };
      return roleMap[info.getValue() as User['role']];
    },
  },
  {
    accessorKey: 'status',
    header: 'ステータス',
    cell: (info) => (
      <StatusBadge
        status={info.getValue() as User['status']}
      />
    ),
  },
  {
    accessorKey: 'createdAt',
    header: '作成日',
    cell: (info) => {
      const date = info.getValue() as Date;
      return date.toLocaleDateString('ja-JP');
    },
  },
];

次に、テーブル本体のコンポーネントを実装します。

typescript// components/UserTable/UserTable.tsx

import { useMemo } from 'react';
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  flexRender,
  SortingState,
} from '@tanstack/react-table';
import { User } from '../../types/user';
import { userColumns } from './columns';

interface UserTableProps {
  data: User[]; // 表示するデータ
  sorting: SortingState; // ソート状態
  onSortingChange: (sorting: SortingState) => void; // ソート変更時
}

TanStack Table のインスタンスを作成します。

typescript// components/UserTable/UserTable.tsx (続き)

export const UserTable = ({
  data,
  sorting,
  onSortingChange,
}: UserTableProps) => {
  // テーブルインスタンスの作成
  const table = useReactTable({
    data,
    columns: userColumns,
    state: {
      sorting,  // ソート状態を管理
    },
    onSortingChange,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
  });

  // ヘッダーとボディを取得
  const headerGroups = table.getHeaderGroups();
  const rows = table.getRowModel().rows;

テーブルの JSX を返します。

typescript// components/UserTable/UserTable.tsx (続き)

  return (
    <div className="overflow-x-auto">
      <table className="min-w-full divide-y divide-gray-200">
        <thead className="bg-gray-50">
          {headerGroups.map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th
                  key={header.id}
                  className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
                  onClick={header.column.getToggleSortingHandler()}
                >
                  {flexRender(
                    header.column.columnDef.header,
                    header.getContext()
                  )}
                </th>
              ))}
            </tr>
          ))}
        </thead>

テーブルボディを実装します。

typescript// components/UserTable/UserTable.tsx (続き)

        <tbody className="bg-white divide-y divide-gray-200">
          {rows.map((row) => (
            <tr key={row.id} className="hover:bg-gray-50">
              {row.getVisibleCells().map((cell) => (
                <td
                  key={cell.id}
                  className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
                >
                  {flexRender(
                    cell.column.columnDef.cell,
                    cell.getContext()
                  )}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

フィルタコンポーネントの実装

検索とフィルタ機能を提供するコンポーネントを作成しましょう。

typescript// components/UserFilter/UserFilter.tsx

import { UserFilterParams } from '../../types/filter';
import { UserRole, UserStatus } from '../../types/user';

interface UserFilterProps {
  filters: UserFilterParams;
  onFilterChange: (filters: UserFilterParams) => void;
}

export const UserFilter = ({
  filters,
  onFilterChange,
}: UserFilterProps) => {
  // 検索テキスト変更時
  const handleSearchChange = (text: string) => {
    onFilterChange({
      ...filters,
      searchText: text || undefined,
      page: 1,  // フィルタ変更時は1ページ目に戻る
    });
  };

役割とステータスのフィルタを実装します。

typescript// components/UserFilter/UserFilter.tsx (続き)

// 役割フィルタ変更時
const handleRoleChange = (role: string) => {
  onFilterChange({
    ...filters,
    role: role ? (role as UserRole) : undefined,
    page: 1,
  });
};

// ステータスフィルタ変更時
const handleStatusChange = (status: string) => {
  onFilterChange({
    ...filters,
    status: status ? (status as UserStatus) : undefined,
    page: 1,
  });
};

フィルタ UI を返します。

typescript// components/UserFilter/UserFilter.tsx (続き)

  return (
    <div className="bg-white p-4 rounded-lg shadow mb-4">
      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
        {/* 検索ボックス */}
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-2">
            検索
          </label>
          <input
            type="text"
            value={filters.searchText || ''}
            onChange={(e) => handleSearchChange(e.target.value)}
            placeholder="名前またはメールで検索"
            className="w-full px-3 py-2 border border-gray-300 rounded-md"
          />
        </div>

役割とステータスのセレクトボックスを実装します。

typescript// components/UserFilter/UserFilter.tsx (続き)

        {/* 役割フィルタ */}
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-2">
            役割
          </label>
          <select
            value={filters.role || ''}
            onChange={(e) => handleRoleChange(e.target.value)}
            className="w-full px-3 py-2 border border-gray-300 rounded-md"
          >
            <option value="">すべて</option>
            <option value="admin">管理者</option>
            <option value="editor">編集者</option>
            <option value="viewer">閲覧者</option>
          </select>
        </div>

        {/* ステータスフィルタ */}
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-2">
            ステータス
          </label>
          <select
            value={filters.status || ''}
            onChange={(e) => handleStatusChange(e.target.value)}
            className="w-full px-3 py-2 border border-gray-300 rounded-md"
          >
            <option value="">すべて</option>
            <option value="active">有効</option>
            <option value="inactive">無効</option>
            <option value="pending">保留中</option>
          </select>
        </div>
      </div>
    </div>
  );
};

ページネーションコンポーネントの実装

ページ遷移を管理するコンポーネントを作成します。

typescript// components/Pagination/Pagination.tsx

interface PaginationProps {
  currentPage: number;      // 現在のページ(1始まり)
  totalPages: number;       // 総ページ数
  onPageChange: (page: number) => void;  // ページ変更時
}

export const Pagination = ({
  currentPage,
  totalPages,
  onPageChange,
}: PaginationProps) => {
  // ページ範囲を計算(現在ページの前後2ページを表示)
  const getPageNumbers = () => {
    const pages: number[] = [];
    const start = Math.max(1, currentPage - 2);
    const end = Math.min(totalPages, currentPage + 2);

    for (let i = start; i <= end; i++) {
      pages.push(i);
    }

    return pages;
  };

  const pageNumbers = getPageNumbers();

ページネーションの UI を実装します。

typescript// components/Pagination/Pagination.tsx (続き)

  return (
    <div className="flex items-center justify-between bg-white px-4 py-3 sm:px-6">
      {/* 前へボタン */}
      <button
        onClick={() => onPageChange(currentPage - 1)}
        disabled={currentPage === 1}
        className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
      >
        前へ
      </button>

      {/* ページ番号 */}
      <div className="flex space-x-2">
        {pageNumbers.map((page) => (
          <button
            key={page}
            onClick={() => onPageChange(page)}
            className={`px-3 py-1 rounded ${
              page === currentPage
                ? 'bg-blue-600 text-white'
                : 'bg-white text-gray-700 hover:bg-gray-50'
            }`}
          >
            {page}
          </button>
        ))}
      </div>

次へボタンを実装します。

typescript// components/Pagination/Pagination.tsx (続き)

      {/* 次へボタン */}
      <button
        onClick={() => onPageChange(currentPage + 1)}
        disabled={currentPage === totalPages}
        className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
      >
        次へ
      </button>
    </div>
  );
};

カスタムフックによる状態管理

テーブルの状態を管理するカスタムフックを作成します。これにより、複数の画面で同じロジックを再利用できますね。

typescript// hooks/useUserTable.ts

import { useState, useEffect } from 'react';
import { SortingState } from '@tanstack/react-table';
import { User } from '../types/user';
import { UserFilterParams } from '../types/filter';

// API からユーザーデータを取得する関数(仮実装)
const fetchUsers = async (
  filters: UserFilterParams
): Promise<{ users: User[]; total: number }> => {
  // 実際には API を呼び出す
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(filters),
  });

  return response.json();
};

カスタムフックの本体を実装します。

typescript// hooks/useUserTable.ts (続き)

export const useUserTable = () => {
  // フィルタ条件の状態
  const [filters, setFilters] = useState<UserFilterParams>({
    page: 1,
    pageSize: 10,
  });

  // ソート状態
  const [sorting, setSorting] = useState<SortingState>([]);

  // データとローディング状態
  const [users, setUsers] = useState<User[]>([]);
  const [total, setTotal] = useState(0);
  const [isLoading, setIsLoading] = useState(false);

データ取得処理を実装します。

typescript// hooks/useUserTable.ts (続き)

  // データ取得
  useEffect(() => {
    const loadUsers = async () => {
      setIsLoading(true);
      try {
        const { users, total } = await fetchUsers(filters);
        setUsers(users);
        setTotal(total);
      } catch (error) {
        console.error('Failed to fetch users:', error);
      } finally {
        setIsLoading(false);
      }
    };

    loadUsers();
  }, [filters]);  // フィルタ変更時に再取得

  // 総ページ数を計算
  const totalPages = Math.ceil(total / filters.pageSize);

  return {
    users,
    total,
    isLoading,
    filters,
    setFilters,
    sorting,
    setSorting,
    totalPages,
  };
};

権限に応じた操作ボタンの実装

権限に基づいて、編集・削除ボタンの表示を制御するコンポーネントを作成しましょう。

typescript// components/UserActions/UserActions.tsx

import { useAuth } from '../../contexts/AuthContext';
import { User } from '../../types/user';

interface UserActionsProps {
  user: User;
  onEdit: (user: User) => void;
  onDelete: (user: User) => void;
}

export const UserActions = ({
  user,
  onEdit,
  onDelete,
}: UserActionsProps) => {
  // 認証情報を取得
  const { canEdit, canDelete } = useAuth();

権限に応じてボタンを表示します。

typescript// components/UserActions/UserActions.tsx (続き)

  return (
    <div className="flex space-x-2">
      {/* 編集ボタン:editor 以上の権限が必要 */}
      {canEdit() && (
        <button
          onClick={() => onEdit(user)}
          className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700"
        >
          編集
        </button>
      )}

      {/* 削除ボタン:admin 権限のみ */}
      {canDelete() && (
        <button
          onClick={() => onDelete(user)}
          className="px-3 py-1 bg-red-600 text-white rounded hover:bg-red-700"
        >
          削除
        </button>
      )}
    </div>
  );
};

管理画面の統合

すべてのコンポーネントを統合したメインの管理画面ページを作成します。

typescript// pages/UserManagement.tsx

import { AuthProvider } from '../contexts/AuthContext';
import { UserTable } from '../components/UserTable/UserTable';
import { UserFilter } from '../components/UserFilter/UserFilter';
import { Pagination } from '../components/Pagination/Pagination';
import { useUserTable } from '../hooks/useUserTable';
import { User } from '../types/user';

// 現在ログイン中のユーザー(実際には認証システムから取得)
const currentUser: User = {
  id: '1',
  name: '山田太郎',
  email: 'yamada@example.com',
  role: 'admin',
  status: 'active',
  createdAt: new Date(),
};

ページコンポーネントを実装します。

typescript// pages/UserManagement.tsx (続き)

export const UserManagement = () => {
  // カスタムフックでテーブル状態を管理
  const {
    users,
    total,
    isLoading,
    filters,
    setFilters,
    sorting,
    setSorting,
    totalPages,
  } = useUserTable();

  // ページ変更ハンドラ
  const handlePageChange = (page: number) => {
    setFilters({ ...filters, page });
  };

JSX を返します。

typescript// pages/UserManagement.tsx (続き)

  return (
    <AuthProvider user={currentUser}>
      <div className="container mx-auto px-4 py-8">
        <h1 className="text-3xl font-bold mb-6">ユーザー管理</h1>

        {/* フィルタ */}
        <UserFilter
          filters={filters}
          onFilterChange={setFilters}
        />

        {/* ローディング表示 */}
        {isLoading ? (
          <div className="text-center py-8">読み込み中...</div>
        ) : (
          <>
            {/* テーブル */}
            <UserTable
              data={users}
              sorting={sorting}
              onSortingChange={setSorting}
            />

            {/* ページネーション */}
            <Pagination
              currentPage={filters.page}
              totalPages={totalPages}
              onPageChange={handlePageChange}
            />

            {/* 件数表示 */}
            <div className="mt-4 text-sm text-gray-600">
              全 {total} 件中 {users.length} 件を表示
            </div>
          </>
        )}
      </div>
    </AuthProvider>
  );
};

以下の図は、実装したコンポーネントの依存関係を示しています。

mermaidflowchart TB
  page["UserManagement<br/>ページ"] --> provider["AuthProvider<br/>権限管理"]

  provider --> filter["UserFilter<br/>フィルタ"]
  provider --> table["UserTable<br/>テーブル表示"]
  provider --> paging["Pagination<br/>ページング"]

  table --> actions["UserActions<br/>操作ボタン"]

  page --> hook["useUserTable<br/>状態管理フック"]
  hook --> api["API 呼び出し<br/>fetchUsers"]

  actions --> authHook["useAuth<br/>権限チェック"]
  authHook --> provider

各コンポーネントが明確な責務を持ち、Context とカスタムフックを通じて連携することで、保守性の高い設計になっていることが分かりますね。

まとめ

本記事では、React と TypeScript を使用して、実務で使える管理画面を最短で構築する方法をご紹介しました。

実装のポイント

TanStack Table の活用により、複雑なテーブル機能を簡潔に実装できました。ヘッドレス UI として設計されているため、デザインの自由度を保ちながら、ソートやフィルタなどの高度な機能を簡単に追加できます。

React Context による権限管理では、アプリケーション全体で一元的に権限を管理することで、セキュリティの実装漏れを防ぎ、保守性を向上させることができましたね。各コンポーネントは useAuth フックを通じて権限情報にアクセスするだけで、複雑なロジックを意識する必要がありません。

カスタムフックによる状態管理は、テーブルの状態(フィルタ、ソート、ページング)を一箇所に集約し、複数の画面で再利用可能にします。これにより、DRY 原則を守りながら、一貫性のある実装が可能になるのです。

パフォーマンスの最適化

今回の実装では、以下のパフォーマンス最適化も考慮しています。

#最適化手法効果
1ページネーション一度に表示するデータ量を制限
2useEffect による遅延読み込みフィルタ変更時のみデータ取得
3TanStack Table の最適化効率的なレンダリング

大量のデータを扱う場合は、さらに仮想化(Virtualization)を導入することで、数万件のデータでもスムーズに表示できるようになります。

今後の拡張性

今回実装した基盤は、以下のような機能拡張にも対応できます。

一括操作機能では、複数のユーザーを選択して一度に編集・削除できるようになるでしょう。TanStack Table の行選択機能を使えば、簡単に実装できます。

詳細フィルタとして、日付範囲や複数条件の AND/OR 検索を追加することも可能です。フィルタ型を拡張するだけで対応できますね。

CSV エクスポート機能を追加すれば、表示中のデータを Excel などで分析できるようになります。データは既に取得済みなので、フロントエンドだけで実装できるのです。

リアルタイム更新では、WebSocket や Server-Sent Events を使用して、他のユーザーの変更をリアルタイムに反映できます。

セキュリティの考慮事項

権限制御は、フロントエンドだけでなく、バックエンドでも必ず実装してください。フロントエンドの権限チェックは UI の表示制御のためであり、セキュリティの最終的な防衛線ではありません。

API エンドポイントでは、以下のチェックを必ず行いましょう。

  • ユーザー認証の確認(JWT トークンの検証など)
  • ユーザーの役割に基づいた操作権限の検証
  • リソースへのアクセス権限の確認(例:他のユーザーのデータを編集できないようにする)

また、XSS 攻撃を防ぐため、ユーザー入力は常にエスケープし、信頼できないデータを直接 HTML にレンダリングしないよう注意が必要です。

本記事で紹介した実装パターンを活用することで、スケーラブルで保守性の高い管理画面を短期間で構築できるでしょう。TypeScript による型安全性と、モダンな React のエコシステムを最大限に活用して、効率的な開発を進めてくださいね。

関連リンク