【Next.js】管理画面で使えるデータテーブルコンポーネントを作成
こんにちは、ライトです。
前回は、管理画面で状態を表示するための StatusBadge コンポーネントを作成しました。
<StatusBadge variant="success">有効</StatusBadge>
<StatusBadge variant="warning">承認待ち</StatusBadge>
<StatusBadge variant="danger">停止中</StatusBadge>ステータスバッジを使うと、ユーザーや商品、注文などの状態を視覚的にわかりやすく表示できます。
今回は、その StatusBadge を活用しながら、管理画面でよく使う データテーブルコンポーネント を作成します。
管理画面では、一覧画面が何度も出てきます。
ユーザー一覧
顧客一覧
商品一覧
注文一覧
売上一覧
ログ一覧
そのたびに table のHTMLやCSSを個別に書いていると、画面ごとに余白や色、空データ表示の見た目がズレやすくなります。
そこで今回は、列定義とデータを渡すだけで一覧を表示できる DataTable コンポーネントを作成します。
今回作成するもの
今回は、以下のように使える DataTable コンポーネントを作成します。
<DataTable
columns={columns}
data={users}
getRowKey={(user) => user.id}
emptyMessage="ユーザーが見つかりません。"
/>今回の DataTable では、以下に対応します。
columnsで列を定義する
dataで表示データを渡す
renderでセル表示をカスタマイズする
空データ表示に対応する
ローディング表示に対応する
横スクロールに対応する
StatusBadgeをテーブル内で使う
いきなり検索、ページネーション、ソートまで入れると少し大きくなるので、今回はまず基本のテーブルコンポーネントを作ります。
検索やページネーションは、次の記事以降で追加していく想定です。
完成形
以下のようなデータテーブルを作成します。

ユーザー一覧を表示し、ステータス列には前回作成した StatusBadge を表示します。
ファイル構成
今回は、以下の構成で作成します。
components/
ui/
StatusBadge.tsx # 前回作成のコンポーネント
DataTable.tsx
app/
page.tsxDataTable は管理画面以外でも使いやすい汎用UIなので、components/ui/ 配下に作成します。
前回作成した StatusBadge も同じ components/ui/ に置いている想定です。
DataTableで実装する機能
今回の DataTable では、以下の機能を実装します。
列定義をpropsで受け取る
表示データをpropsで受け取る
行のkeyを指定できる
セルの表示をrenderで自由に変えられる
データが空の場合の表示に対応する
ローディング中の表示に対応する
横幅が足りない場合は横スクロールする
管理画面の一覧では、単純な文字列だけでなく、バッジやボタン、リンクなどを表示したいことがあります。
そのため、セルの中身は render で自由に指定できるようにします。
DataTable.tsxを作成する
それでは、DataTable.tsx を作成します。
components/ui/DataTable.tsx
import type { ReactNode } from "react";
export type DataTableColumn<T> = {
key: string;
header: string;
render: (item: T) => ReactNode;
className?: string;
};
type DataTableProps<T> = {
columns: DataTableColumn<T>[];
data: T[];
getRowKey: (item: T) => string | number;
emptyMessage?: string;
isLoading?: boolean;
};
export default function DataTable<T>({
columns,
data,
getRowKey,
emptyMessage = "データがありません。",
isLoading = false,
}: DataTableProps<T>) {
return (
<div className="overflow-hidden rounded-lg border border-slate-200 bg-white">
<div className="overflow-x-auto">
<table className="w-full min-w-[720px] text-left text-sm">
<thead className="bg-slate-50 text-xs font-medium text-slate-500">
<tr>
{columns.map((column) => (
<th
key={column.key}
className={["px-6 py-3", column.className ?? ""].join(" ")}
>
{column.header}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{isLoading &&
Array.from({ length: 5 }).map((_, rowIndex) => (
<tr key={rowIndex}>
{columns.map((column) => (
<td key={column.key} className="px-6 py-4">
<div className="h-4 w-24 animate-pulse rounded bg-slate-100" />
</td>
))}
</tr>
))}
{!isLoading &&
data.map((item) => (
<tr
key={getRowKey(item)}
className="transition hover:bg-slate-50/80"
>
{columns.map((column) => (
<td
key={column.key}
className={["px-6 py-4", column.className ?? ""].join(
" "
)}
>
{column.render(item)}
</td>
))}
</tr>
))}
{!isLoading && data.length === 0 && (
<tr>
<td
colSpan={columns.length}
className="px-6 py-12 text-center text-sm text-slate-500"
>
{emptyMessage}
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
}これで、基本的なデータテーブルコンポーネントができました。
columnsで列を定義する
DataTable では、列の情報を columns として渡します。
export type DataTableColumn<T> = {
key: string; # 列を識別するための値
header: string; # テーブルヘッダーに表示する文字
render: (item: T) => ReactNode; # セルの中身を表示する関数
className?: string; # 列ごとに追加したいクラス
};たとえば、ユーザー一覧の場合は次のように定義できます。
const columns: DataTableColumn<User>[] = [
{
key: "name",
header: "名前",
render: (user) => user.name,
},
{
key: "email",
header: "メールアドレス",
render: (user) => user.email,
},
];このようにしておくと、DataTable 側はデータの中身を知る必要がありません。
表示方法は、使う側が columns で指定します。
renderでセル表示をカスタマイズする
render を使うことで、セルの中身を自由に作れます。
たとえば、名前の列では、名前だけでなくIDも表示できます。
{
key: "name",
header: "名前",
render: (user) => (
<div>
<div className="font-medium text-slate-900">{user.name}</div>
<div className="mt-1 text-xs text-slate-400">ID: {user.id}</div>
</div>
),
}メールアドレスは、文字色を少し薄くして表示できます。
{
key: "email",
header: "メールアドレス",
render: (user) => (
<span className="text-slate-600">{user.email}</span>
),
}このように、render を用意しておくと、単純なテキストだけでなく、装飾付きの表示や別コンポーネントも使えます。
StatusBadgeをテーブル内で使う
前回作成した StatusBadge も、render の中で使えます。
function renderStatus(status: User["status"]) {
switch (status) {
case "active":
return <StatusBadge variant="success">有効</StatusBadge>;
case "pending":
return <StatusBadge variant="warning">承認待ち</StatusBadge>;
case "suspended":
return <StatusBadge variant="danger">停止中</StatusBadge>;
}
}列定義では、次のように使います。
{
key: "status",
header: "ステータス",
render: (user) => renderStatus(user.status),
}これで、ステータス列に色付きのバッジを表示できます。
管理画面では状態を見分けることが多いため、テーブルとステータスバッジはかなり相性が良いです。
空データ表示に対応する
一覧画面では、データが0件の場合もあります。
そのため、DataTable では data.length === 0 のときに空データ表示を出します。
{!isLoading && data.length === 0 && (
<tr>
<td
colSpan={columns.length}
className="px-6 py-12 text-center text-sm text-slate-500"
>
{emptyMessage}
</td>
</tr>
)}colSpan={columns.length} を指定しているので、すべての列をまたいでメッセージを表示できます。
使う側では、emptyMessage を指定できます。
<DataTable
columns={columns}
data={users}
getRowKey={(user) => user.id}
emptyMessage="ユーザーが見つかりません。"
/>データがないときの表示も共通化しておくと、一覧画面ごとのばらつきを減らせます。
ローディング表示に対応する
APIからデータを取得する場合、読み込み中の状態も必要になります。
今回は、isLoading が true のときに簡単なスケルトン表示を出します。
{isLoading &&
Array.from({ length: 5 }).map((_, rowIndex) => (
<tr key={rowIndex}>
{columns.map((column) => (
<td key={column.key} className="px-6 py-4">
<div className="h-4 w-24 animate-pulse rounded bg-slate-100" />
</td>
))}
</tr>
))}animate-pulse を使うことで、読み込み中であることがわかりやすくなります。
<div className="h-4 w-24 animate-pulse rounded bg-slate-100" />今回はシンプルに各セルへ同じスケルトンを表示しています。
必要であれば、列ごとに幅を変えたり、行数をpropsで指定できるようにしてもよいです。
横スクロールに対応する
管理画面のテーブルは、列数が増えることがあります。
モバイルやタブレットでは横幅が足りなくなることもあるため、横スクロールに対応しておきます。
<div className="overflow-x-auto">
<table className="w-full min-w-[720px] text-left text-sm">
...
</table>
</div>overflow-x-auto を指定することで、横幅が足りない場合に横スクロールできます。
また、テーブルには min-w-[720px] を指定しています。
これにより、狭い画面でも列が潰れすぎないようにしています。
管理画面レイアウトに配置する
次に、管理画面レイアウトの中で DataTable を使ってみます。
今回はユーザー一覧を例にします。
app/page.tsx
import DashboardLayout from "@/components/dashboard/DashboardLayout";
import DataTable, { DataTableColumn } from "@/components/ui/DataTable";
import StatusBadge from "@/components/ui/StatusBadge";
type User = {
id: number;
name: string;
email: string;
role: string;
status: "active" | "pending" | "suspended";
};
const users: User[] = [
{
id: 1,
name: "山田 太郎",
email: "yamada@example.com",
role: "管理者",
status: "active",
},
{
id: 2,
name: "佐藤 花子",
email: "sato@example.com",
role: "編集者",
status: "pending",
},
{
id: 3,
name: "田中 一郎",
email: "tanaka@example.com",
role: "閲覧者",
status: "suspended",
},
];
function renderStatus(status: User["status"]) {
switch (status) {
case "active":
return <StatusBadge variant="success">有効</StatusBadge>;
case "pending":
return <StatusBadge variant="warning">承認待ち</StatusBadge>;
case "suspended":
return <StatusBadge variant="danger">停止中</StatusBadge>;
}
}
const columns: DataTableColumn<User>[] = [
{
key: "name",
header: "名前",
render: (user) => (
<div>
<div className="font-medium text-slate-900">{user.name}</div>
<div className="mt-1 text-xs text-slate-400">ID: {user.id}</div>
</div>
),
},
{
key: "email",
header: "メールアドレス",
render: (user) => (
<span className="text-slate-600">{user.email}</span>
),
},
{
key: "role",
header: "権限",
render: (user) => (
<span className="text-slate-600">{user.role}</span>
),
},
{
key: "status",
header: "ステータス",
render: (user) => renderStatus(user.status),
},
];
export default function Page() {
return (
<DashboardLayout>
<div className="space-y-6">
<section>
<p className="text-sm font-medium text-primary-700">Users</p>
<h2 className="mt-1 text-2xl font-semibold text-slate-950">
ユーザー一覧
</h2>
<p className="mt-2 max-w-2xl text-sm leading-7 text-slate-600">
管理画面で使うデータテーブルコンポーネントです。
ステータス列には、前回作成したStatusBadgeを表示しています。
</p>
</section>
<DataTable
columns={columns}
data={users}
getRowKey={(user) => user.id}
emptyMessage="ユーザーが見つかりません。"
/>
</div>
</DashboardLayout>
);
}これで、管理画面レイアウト内にユーザー一覧テーブルを表示できます。

ステータス列には、前回作成した StatusBadge が表示されます。
ローディング状態を確認する
ローディング状態を確認したい場合は、isLoading を true にします。
<DataTable
columns={columns}
data={[]}
getRowKey={(user) => user.id}
isLoading
/>この場合、データの代わりにスケルトン表示が出ます。

APIからデータを取得する実装にした場合は、取得中だけ isLoading を true にするとよさそうです。
空データ状態を確認する
空データ状態を確認する場合は、data に空配列を渡します。
<DataTable
columns={columns}
data={[]}
getRowKey={(user) => user.id}
emptyMessage="ユーザーが見つかりません。"
/>データがない場合は、指定したメッセージが表示されます。

一覧画面では、検索結果が0件のときにも同じ仕組みを使えます。
実装のポイント
今回のポイントは、テーブルの見た目と表示ロジックを共通化したことです。
DataTable
テーブルの枠、ヘッダー、行、空データ、ローディングを担当columns
表示する列を定義render
セルの中身を自由に作るStatusBadge
状態表示を見やすくする
DataTable 側は、データの中身を知りません。
どの列を表示するか、どのように表示するかは columns で指定します。
const columns: DataTableColumn<User>[] = [
{
key: "status",
header: "ステータス",
render: (user) => renderStatus(user.status),
},
];このようにしておくと、ユーザー一覧だけでなく、顧客一覧、商品一覧、注文一覧などにも使い回せます。
さらに改善できそうな点
今回の実装では、基本的なデータテーブルに絞りました。
さらに改善するなら、以下のような機能も追加できそうです。
検索・絞り込みに対応する
ページネーションを追加する
ソートに対応する
行クリックで詳細画面へ遷移する
編集・削除ボタンを追加する
チェックボックス選択に対応する
列ごとの幅指定に対応する
ローディング行数をpropsで指定できるようにする
特に、管理画面では検索とページネーションがよく使われます。
次は、今回作成した DataTable に組み合わせる形で、検索・絞り込みバーやページネーションを作成すると、より実務に近い一覧画面になりそうです。
まとめ
今回は、Next.jsで管理画面に使える DataTable コンポーネントを作成しました。
今回の実装では、以下に対応しました。
columnsで列を定義する
dataで表示データを渡す
renderでセルの中身を自由に表示する
空データ表示に対応する
ローディング表示に対応する
横スクロールに対応する
StatusBadgeをテーブル内で使う
管理画面では、一覧画面が何度も出てきます。
そのたびにテーブルのHTMLや見た目を個別に作るのではなく、DataTable として共通化しておくと、画面ごとの実装が楽になります。
また、前回作成した StatusBadge を組み込むことで、状態もわかりやすく表示できます。
次は、この DataTable に検索やページネーションを組み合わせて、より実用的な一覧画面にしていきたいと思います。
いいなと思ったら応援しよう!
もしよろしければ応援をお願いいたします。
いただいたチップでコーヒーを飲んでがんばります!