ドキュメント
データテーブル

データテーブル

TanStack Tableを使用して構築された、強力なテーブルおよびデータグリッドです。

はじめに

私がこれまで作成してきたデータテーブルやデータグリッドは、どれもユニークなものでした。それぞれが異なる動作をし、特定のソートやフィルタリングの要件を持ち、異なるデータソースと連携します。

これらすべてのバリエーションを単一のコンポーネントにまとめるのは理にかなっていません。もしそうしてしまうと、ヘッドレスUIが提供する柔軟性を失ってしまいます。

そこで、データテーブルコンポーネントを提供する代わりに、独自のデータテーブルを構築する方法に関するガイドを提供することがより役立つと考えました。

基本的な<Table />コンポーネントから始め、複雑なデータテーブルをゼロから構築していきます。

ヒント: アプリ内の複数の場所で同じテーブルを使用することになった場合は、いつでも再利用可能なコンポーネントとして抽出することができます。

目次

このガイドでは、TanStack Table<Table />コンポーネントを使用して、独自のカスタムデータテーブルを構築する方法を説明します。以下のトピックを扱います。

インストール

  1. プロジェクトに<Table />コンポーネントを追加する
npx shadcn-solid@latest add table button dropdown-menu textfield checkbox
  1. tanstack/solid-tableの依存関係を追加する
npm install @tanstack/solid-table

前提条件

タスクを表示するためのテーブルを構築します。使用するデータは次のようになります。

type Task = {
  id: string;
  code: string;
  title: string;
  status: "todo" | "in-progress" | "done" | "cancelled";
  label: "bug" | "feature" | "enhancement" | "documentation";
};
 
export const tasks: Task[] = [
  {
    id: "ptL0KpX_yRMI98JFr6B3n",
    code: "TASK-33",
    title: "We need to bypass the redundant AI interface!",
    status: "todo",
    label: "bug"
  },
  {
    id: "RsrTg_SmBKPKwbUlr7Ztv",
    code: "TASK-59",
    title:
      "Overriding the capacitor won't do anything, we need to generate the solid state JBOD pixel!",
    status: "in-progress",
    label: "feature"
  }
  // ...
];

プロジェクト構成

まず、以下のファイル構成を作成します。

src
└── routes
    ├── _components
    │   ├── columns.tsx
    │   └── data-table.tsx
    └── index.tsx
  • columns.tsxには、列の定義が含まれます。
  • data-table.tsxには、<DataTable />コンポーネントが含まれます。
  • index.tsxは、データを取得してテーブルを描画する場所です。

基本的なテーブル

まずは基本的なテーブルの構築から始めましょう。

列の定義

最初に、列を定義します。

src/routes/_components/columns.tsx
import type { ColumnDef } from "@tanstack/solid-table";
 
// This type is used to define the shape of our data.
// You can use a Zod or Validbot schema here if you want.
export type Task = {
  id: string;
  code: string;
  title: string;
  status: "todo" | "in-progress" | "done" | "cancelled";
  label: "bug" | "feature" | "enhancement" | "documentation";
};
 
export const columns: ColumnDef<Task>[] = [
  {
    accessorKey: "code",
    header: "Task"
  },
  {
    accessorKey: "title",
    header: "Title"
  },
  {
    accessorKey: "status",
    header: "Status"
  }
];

<DataTable />コンポーネント

次に、テーブルを描画するための<DataTable />コンポーネントを作成します。

src/routes/_components/data-table.tsx
import type { ColumnDef } from "@tanstack/solid-table";
import { flexRender, createSolidTable, getCoreRowModel } from "@tanstack/solid-table";
import { For, Show, splitProps, Accessor } from "solid-js";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow
} from "@/components/ui/table";
 
type Props<TData, TValue> = {
  columns: ColumnDef<TData, TValue>[];
  data: Accessor<TData[] | undefined>;
};
 
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
  const [local] = splitProps(props, ["columns", "data"]);
 
  const table = createSolidTable({
    get data() {
      return local.data() || [];
    },
    columns: local.columns,
    getCoreRowModel: getCoreRowModel()
  });
 
  return (
    <div class="rounded-md border">
      <Table>
        <TableHeader>
          <For each={table.getHeaderGroups()}>
            {headerGroup => (
              <TableRow>
                <For each={headerGroup.headers}>
                  {header => {
                    return (
                      <TableHead>
                        {header.isPlaceholder
                          ? null
                          : flexRender(header.column.columnDef.header, header.getContext())}
                      </TableHead>
                    );
                  }}
                </For>
              </TableRow>
            )}
          </For>
        </TableHeader>
        <TableBody>
          <Show
            when={table.getRowModel().rows?.length}
            fallback={
              <TableRow>
                <TableCell colSpan={local.columns.length} class="h-24 text-center">
                  No results.
                </TableCell>
              </TableRow>
            }
          >
            <For each={table.getRowModel().rows}>
              {row => (
                <TableRow data-state={row.getIsSelected() && "selected"}>
                  <For each={row.getVisibleCells()}>
                    {cell => (
                      <TableCell>
                        {flexRender(cell.column.columnDef.cell, cell.getContext())}
                      </TableCell>
                    )}
                  </For>
                </TableRow>
              )}
            </For>
          </Show>
        </TableBody>
      </Table>
    </div>
  );
};

テーブルを描画する

最後に、indexページでテーブルを描画します。

src/routes/index.tsx
import type { Task } from "./_components/columns";
import { columns } from "./_components/columns";
import { DataTable } from "./_components/data-table";
import type { RouteDefinition } from "@solidjs/router";
import { cache, createAsync } from "@solidjs/router";
 
const getData = cache(async (): Promise<Task[]> => {
  // Fetch data from your API here.
  return [
    {
      id: "ptL0KpX_yRMI98JFr6B3n",
      code: "TASK-33",
      title: "We need to bypass the redundant AI interface!",
      status: "todo",
      label: "bug"
    }
    // ...
  ];
}, "data");
 
export const route: RouteDefinition = {
  load: () => getData()
};
 
const Home = () => {
  const data = createAsync(() => getData());
 
  return (
    <div class="w-full space-y-2.5">
      <DataTable columns={columns} data={data} />
    </div>
  );
};
 
export default Home;

行アクション

列の定義を更新して、新しいactions列を追加します。actionsセルは<Dropdown />コンポーネントを返します。

src/routes/_components/columns.tsx
//...
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
 
export const columns: ColumnDef<Task>[] = [
  // ...
  {
    id: "actions",
    cell: () => (
      <DropdownMenu placement="bottom-end">
        <DropdownMenuTrigger class="flex items-center justify-center">
          <svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24">
            <path
              fill="none"
              stroke="currentColor"
              stroke-linecap="round"
              stroke-linejoin="round"
              stroke-width="2"
              d="M4 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0"
            />
          </svg>
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem>Edit</DropdownMenuItem>
          <DropdownMenuItem>Delete</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    )
  }
  // ...
];

cell関数内でrow.originalを使用して行データにアクセスできます。これを使用して、行のアクションを処理します。例えば、idを使用してAPIへのDELETEリクエストを行うなどです。

ページネーション

次に、テーブルにページネーションを追加します。

<DataTable>を更新する

src/routes/_components/data-table.tsx
//...
import {
  //...
  getPaginationRowModel
} from "@tanstack/solid-table";
 
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
  const table = createSolidTable({
    //...
    getPaginationRowModel: getPaginationRowModel()
  });
 
  // ...
};

これにより、行が自動的に10件ごとのページに分割されます。ページサイズのカスタマイズや手動ページネーションの実装に関する詳細については、ページネーションのドキュメントを参照してください。

ページネーションコントロールを追加する

<Button />コンポーネントとtable.previousPage()table.nextPage() APIメソッドを使用して、テーブルにページネーションコントロールを追加できます。

src/routes/_components/data-table.tsx
//...
import { Button } from "@/components/ui/button"
 
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) {
  //...
 
  return (
    <div>
      <div class="rounded-md border">...
      </div>
      <div class="flex items-center justify-end space-x-2 py-4">
        <Button
          variant="outline"
          size="sm"
          onClick={() => table.previousPage()}
          disabled={!table.getCanPreviousPage()}
        >
          Previous
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => table.nextPage()}
          disabled={!table.getCanNextPage()}
        >
          Next
        </Button>
      </div>
    </div>
  )
}

ソート

<DataTable>を更新する

src/routes/_components/data-table.tsx
//...
import {
  //...
  getSortedRowModel
} from "@tanstack/solid-table";
 
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
  const [sorting, setSorting] = createSignal<SortingState>([]);
 
  const table = createSolidTable({
    //...
    onSortingChange: setSorting,
    getSortedRowModel: getSortedRowModel(),
    state: {
      get sorting() {
        return sorting();
      }
    }
  });
 
  // ...
};

ヘッダーセルをソート可能にする

statusのヘッダーセルを更新して、ソートコントロールを追加します。

src/routes/_components/columns.tsx
//...
import { Button } from "@/components/ui/button";
 
export const columns: ColumnDef<Task>[] = [
  // ...
  {
    accessorKey: "status",
    header: ({ column }) => {
      return (
        <Button
          variant="ghost"
          onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
        >
          Status
          <svg
            xmlns="http://www.w3.org/2000/svg"
            class="ml-2 size-4"
            aria-hidden="true"
            viewBox="0 0 24 24"
          >
            <path
              fill="none"
              stroke="currentColor"
              stroke-linecap="round"
              stroke-linejoin="round"
              stroke-width="2"
              d="M12 5v14m4-4l-4 4m-4-4l4 4"
            />
          </svg>
        </Button>
      );
    }
  }
  // ...
];

フィルタリング

titleにフィルターを追加します。フィルターのカスタマイズに関する詳細については、フィルタリングのドキュメントを参照してください。

<DataTable>を更新する

src/routes/_components/data-table.tsx
//...
import {
  //...
  getFilteredRowModel
} from "@tanstack/solid-table";
import { TextField, TextFieldInput } from "~/components/ui/textfield";
 
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
  //...
  const [columnFilters, setColumnFilters] = createSignal<ColumnFiltersState>([]);
 
  const table = createSolidTable({
    //...
    getFilteredRowModel: getFilteredRowModel(),
    state: {
      //...
      get columnFilters() {
        return columnFilters();
      }
    }
  });
 
  <div>
    <div class="flex items-center py-4">
      <TextField>
        <TextFieldInput
          placeholder="Filter title..."
          value={(table.getColumn("title")?.getFilterValue() as string) ?? ""}
          onInput={event => table.getColumn("title")?.setFilterValue(event.currentTarget.value)}
          class="max-w-sm"
        />
      </TextField>
    </div>
    <div class="rounded-md border">...</div>
  </div>;
};

表示切替

visibility APIを使用して列の表示/非表示機能を追加する

<DataTable>を更新する

src/routes/_components/data-table.tsx
//...
import { As } from "@kobalte/core";
import { Button } from "~/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
 
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
  //...
  const [columnVisibility, setColumnVisibility] = createSignal<VisibilityState>({});
 
  const table = createSolidTable({
    //...
    onColumnVisibilityChange: setColumnVisibility,
    state: {
      //...
      get columnVisibility() {
        return columnVisibility();
      }
    }
  });
 
  <div>
    <div class="flex items-center py-4">
      //...
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <As component={Button} variant="outline" class="ml-auto">
            Columns
          </As>
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <For each={table.getAllColumns().filter(column => column.getCanHide())}>
            {item => (
              <DropdownMenuCheckboxItem
                class="capitalize"
                checked={item.getIsVisible()}
                onChange={value => item.toggleVisibility(!!value)}
              >
                {item.id}
              </DropdownMenuCheckboxItem>
            )}
          </For>
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
    <div class="rounded-md border">...</div>
  </div>;
};

行選択

次に、テーブルに行選択機能を追加します。

列の定義を更新する

src/routes/_components/columns.tsx
//...
import { Checkbox, CheckboxControl } from "@/components/ui/checkbox";
 
export const columns: ColumnDef<Task>[] = [
  {
    id: "select",
    header: ({ table }) => (
      <Checkbox
        indeterminate={table.getIsSomePageRowsSelected()}
        checked={table.getIsAllPageRowsSelected()}
        onChange={value => table.toggleAllPageRowsSelected(!!value)}
        aria-label="Select all"
      >
        <CheckboxControl />
      </Checkbox>
    ),
    cell: ({ row }) => (
      <Checkbox
        checked={row.getIsSelected()}
        onChange={value => row.toggleSelected(!!value)}
        aria-label="Select row"
      >
        <CheckboxControl />
      </Checkbox>
    ),
    enableSorting: false,
    enableHiding: false
  }
  // ...
];

<DataTable>を更新する

src/routes/_components/data-table.tsx
//...
 
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
  //...
  const [rowSelection, setRowSelection] = createSignal({});
 
  const table = createSolidTable({
    //...
    onRowSelectionChange: setRowSelection,
    state: {
      // ...
      get rowSelection() {
        return rowSelection();
      }
    }
  });
 
  <div>...</div>;
};

選択された行を表示する

table.getFilteredSelectedRowModel() APIを使用して、選択された行の数を表示できます。

<div class="text-muted-foreground flex-1 text-sm">
  {table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length}{" "}
  row(s) selected.
</div>

shadcn によって構築・デザインされました。 hngngn によってSolidへ移植されました。ソースコードは GitHub で公開されています。