RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

【React】tanstack table + MUIでチェックボックス付きのテーブルを作る

はじめに

こんにちは。フロントエンド開発課に所属している新卒1年目のm_you_sanと申します。
実務でtanstack tableを使う機会があり、便利に感じたので紹介させていただきます。
目次は以下の通りです。

tanstack tableとは

tanstack tableとはテーブルを容易に実装できるヘッドレスUIライブラリです。
ヘッドレスなので、UIとロジックを制御する部分を独立させることができ、柔軟にUIを制御することができます。

v7まではreact-tableという名前でしたが、メジャーアップデート後にtanstack tableになり、react以外にvue、solidなどのフレームワークにも対応しています。

使い方

使い方について、サンプルコードを交えて解説します。
今回はtanstack tableとMUIを使用して、チェックボックス付きのテーブルを作成していきます。
tanstack tableを初めて使う初学者の方は、是非、手を動かしながら本記事を読んでいただければと思います。
インストール方法については、公式ドキュメントを参照してください。
MUI tanstack table

サンプルデータの用意

まず初めにテーブルに表示するサンプルデータを用意します。
今回は50個のデータを作成し、tasksの初期値とします。
その後、tasksをpropsとして子のCheckBoxTableに渡します。
App.tsx

export type Task = {
  id: number;
  name: string;
  isDone: boolean;
}

const App: React.FC = () => {
 
 const defaultData: Task[] = [...Array(50).keys()].map((i) => ({
  id: i + 1,
  name: `タスク${i + 1}`,
  isDone: i % 2 === 0? true : false
 }));

 const  [tasks, setTasks] = useState(defaultData);

 return  <CheckBoxTable data={tasks} />
  
}

カラムの定義

次にCheckBoxTableの中身を作成していきます。
初めにカラムを定義します。
ColumnDefに型情報を設定することで、tableやrowにも型情報が反映されます。

headerは列のヘッダーの表示内容となっており、 カスタムコンポーネントやテキストなどを使用して表示内容をカスタマイズすることができます。
今回はMUIと組み合わせて表示内容をカスタマイズしています。
また、headerにはtableを引数に設定して、tableのAPIを使用することができます。

cellは列内の各セルの表示内容です。
こちらもheaderと同様に表示内容をカスタマイズできます。
また、cellにはrowを引数に設定することができ、rowのAPIを使用することができます。

なお、チェックボックスのハンドラーについてですが、table APIとrow APIで用意されているので、自分で作成する必要はありません。

CheckBoxTable.tsx

type Props = {
  data: Task[];
}

export const CheckBoxTable: React.FC<Props> = ({data}) => {

 const columns: ColumnDef<Task>[] = [
    {
      id: 'select',
      header: ({ table }) => (
        <Checkbox
          //現在のページの全ての行が選択されているかどうか
          checked={table.getIsAllRowsSelected()}
          //全ての行のチェックボックスを切り替えるために使用するハンドラーを返す
          onChange={table.getToggleAllRowsSelectedHandler()}
        />
      ),
      cell: ({ row }) => (
        <Checkbox
          //行が選択されているかどうか
          checked={row.getIsSelected()}
        //未実施の場合は非活性
          disabled={!row.original.isDone}
          //チェックボックスを切り替えるために使用するハンドラーを返す
          onChange={row.getToggleSelectedHandler()}
        />
      )
    },
    {
      id: 'id',
      header: 'タスクID',
      accessorKey: 'id'
    },
    {
      id: 'name',
      header: 'タスク名',
      accessorKey: 'name'
    },
    {
      id: 'isDone',
      header: 'タスクの実施状況',
      cell: ({row}) => row.original.isDone? '実施済' : '未実施'
    }
  ]
}

テーブルオブジェクトの作成

次にテーブルオブジェクトを作成します。
テーブルオブジェクトの作成には、useReactTableを使用し、テーブルに表示するデータ、カラムを設定します。
getCoreRowModelはテーブルの行モデルを計算して返す関数となっていますが、あまり難しいことは考えずに、getCoreRowModel()を設定してあげれば良いと思います。
なお、data、columns、getCoreRowModelは必須オプションなので、どれか1つ欠けているとエラーが発生します。
CheckBoxTable.tsx

const table = useReactTable<Task>({
    data,
    columns,
    getCoreRowModel: getCoreRowModel()
})

テーブルの表示

次にテーブルを表示させます。
getHeaderGroupはテーブルのヘッダー情報、getCoreRowModelはテーブルの行の情報を返します。
getHeaderGroupで返されるヘッダー情報は、配列になっており、それらをmap関数で展開し、flexRender関数を使用して表示させています。
Image from Gyazo

getCoreRowModelで返される行の情報についても、配列になっており、ヘッダーと同じ方法で表示させています。
Image from Gyazo

CheckBoxTable.tsx

return (
   <Table>
    <TableHead>
      {table.getHeaderGroups().map((headerGroup) => (
        <TableRow key={headerGroup.id}>
          {headerGroup.headers.map((header) => (
            <TableCell key={header.id}>
              {flexRender(header.column.columnDef.header, header.getContext())}
            </TableCell>
          ))}
        </TableRow>
      ))}
    </TableHead>
    <TableBody>
      {table.getCoreRowModel().rows.map((row) => (
        <TableRow key={row.id}>
          {row.getVisibleCells().map((cell) => (
            <TableCell key={cell.id}>
              {flexRender(cell.column.columnDef.cell, cell.getContext())}
            </TableCell>
          ))}
        </TableRow>
      ))}
    </TableBody>
   </Table>
  )

画面を確認すると、このようにテーブルが表示されているのが分かります。
Image from Gyazo

なお、カラムを定義した際に、cellを使って表示内容を指定していない場合は、accessorKeyが抜けていると上手く表示されません。

    {
      id: 'id',
      header: 'タスクID',
      // accessorKey: 'id'
    },
    {
      id: 'name',
      header: 'タスク名',
      // accessorKey: 'name'
    },

Image from Gyazo

チェックボックスの状態管理

テーブルの見た目は実装できたので、次に状態管理を追加します。
先程のテーブルオブジェクトにオプションを追加します。
CheckBoxTable.tsx

const [rowSelection, setRowSelection] = useState({});

//中略

const table = useReactTable<Task>({
    data,
    columns,
    state: { rowSelection },
    onRowSelectionChange: setRowSelection,
    getCoreRowModel: getCoreRowModel(),
    //実施済のタスクだけ選択されるように設定
    enableRowSelection: (row) => row.original.isDone
})

上記のコードでは、stateに状態管理する値を渡しており、onRowSelectionChangeでチェックボックスが切り替わったときにセッター関数を呼び出して、状態を更新しています。
状態管理しているrowSelectionはエディタなどで確認すると、RowSelectionTableStateとundefinedのユニオン型になっていることが分かります。
RowSelectionTableState型は公式ドキュメントを確認してみると、以下のようになっています。

export type RowSelectionState = Record<string, boolean>

export type RowSelectionTableState = {
  rowSelection: RowSelectionState
}

つまり、チェックボックスが切り替わったとき、rowSelectionはstring型のキーとboolean型のvalueを保持することが考えられます。
実際にrowSelectionの中身をコンソールで見ると、キーは選択されている各行のインデックスで、valueはtrueになっていることが分かります。
Image from Gyazo

また、ここまで度々登場しているrow.originalについても、コンソールで中身を見ていきたいと思います。
row.originalには、tableオブジェクトに設定したデータの情報が入っているのが分かります。
Image from Gyazo
今までの「cell: ({row}) => row.original.isDone? '実施済' : '未実施'」や「enableRowSelection: (row) => row.original.isDone」は、originalにあるisDoneを使って、true、falseを取得していたということです。

行の削除

最後に、選択した行を削除する機能を作成します。
今回作成する削除機能は、選択した行のidとタスクのidが一致するものをfilter関数で除くという簡易的なものになっています。
App.tsx

const App: React.FC = () => {

 const defaultData: Task[] = [...Array(50).keys()].map((i) => ({
  id: i + 1,
  name: `タスク${i + 1}`,
  isDone: i % 2 === 0? true : false
 }));

 const  [tasks, setTasks] = useState(defaultData);

 const handleDelete = (ids: number[]) => {
  const updateTasks = tasks.filter((task) => !ids.includes(task.id));
  setTasks(updateTasks);
 }

  return <CheckBoxTable data={tasks} onDelete={handleDelete} />
}

checkBoxTableにも変更を加えていきます。
まず、行が選択された状態でなければ、削除ボタンを押せないようにするため、isSelected がfalseの場合、非活性にしています。
そして、削除する際に選択されている行のidが必要なので、table.getSelectedRowModel()で選択されている行の情報を取得し、map関数で行のidを含む配列を作成しています。
作成した配列をonDeleteの引数にし、削除が完了したら、選択状態がリセットされるようにします。

CheckBoxTable.tsx

type Props = {
  data: Task[];
  onDelete: (ids: number[]) => void;
}

export const CheckBoxTable: React.FC<Props> = ({data, onDelete}) => {

  //中略

  //行のどれか1つでも選択されていればtrueを返す
  const isSelected = table.getIsSomeRowsSelected();
  
  //選択している行のidを配列にする
  const ids = table.getSelectedRowModel().rows.map((row) => row.original.id);

  const handleDelete = () => {
    onDelete(ids);
    //選択状態をリセット
    table.resetRowSelection();
  }

  return (
   <>
    <button 
      // 何も選択されていなければ非活性
      disabled={!isSelected} 
      onClick={handleDelete} 
    >削除</button>
    <Table>
      <TableHead>
        {table.getHeaderGroups().map((headerGroup) => (
          <TableRow key={headerGroup.id}>
            {headerGroup.headers.map((header) => (
              <TableCell key={header.id}>
                {flexRender(header.column.columnDef.header, header.getContext())}
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableHead>
      <TableBody>
        {table.getCoreRowModel().rows.map((row) => (
          <TableRow key={row.id}>
            {row.getVisibleCells().map((cell) => (
              <TableCell key={cell.id}>
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
   </>
  )
}

最後に画面で動作を確認してみます。
以下のように、動作していれば実装完了です。
Image from Gyazo

上手く動作しない場合は、以下にコード全体を載せているので、そちらと見比べて、ご確認いただければと思います。
App.tsx

export type Task = {
  id: number;
  name: string;
  isDone: boolean;
}

const App: React.FC = () => {

 const defaultData: Task[] = [...Array(50).keys()].map((i) => ({
  id: i + 1,
  name: `タスク${i + 1}`,
  isDone: i % 2 === 0? true : false
 }));

 const  [tasks, setTasks] = useState(defaultData);

 const handleDelete = (ids: number[]) => {
  const updateTasks = tasks.filter((task) => !ids.includes(task.id));
  setTasks(updateTasks);
 }

  return <CheckBoxTable data={tasks} onDelete={handleDelete} />
}

CheckBoxTable.tsx

type Props = {
  data: Task[];
  onDelete: (ids: number[]) => void;
}

export const CheckBoxTable: React.FC<Props> = ({data, onDelete}) => {

  const [rowSelection, setRowSelection] = useState({});

  const columns: ColumnDef<Task>[] = [
    {
      id: 'select',
      header: ({ table }) => (
        <Checkbox
          checked={table.getIsAllRowsSelected()}
          onChange={table.getToggleAllRowsSelectedHandler()}
        />
      ),
      cell: ({ row }) => (
        <Checkbox
          checked={row.getIsSelected()}
          disabled={!row.original.isDone}
          onChange={row.getToggleSelectedHandler()}
        />
      )
    },
    {
      id: 'id',
      header: 'タスクID',
      accessorKey: 'id'
    },
    {
      id: 'name',
      header: 'タスク名',
      accessorKey: 'name'
    },
    {
      id: 'isDone',
      header: 'タスクの実施状況',
      cell: ({row}) => row.original.isDone? '実施済' : '未実施'
    }
  ]

  const table = useReactTable<Task>({
    data,
    columns,
    state: { rowSelection },
    onRowSelectionChange: setRowSelection,
    getCoreRowModel: getCoreRowModel(),
    enableRowSelection: (row) => row.original.isDone
  })

  const isSelected = table.getIsSomeRowsSelected();
  
  const ids = table.getSelectedRowModel().rows.map((row) => row.original.id);

  const handleDelete = () => {
    onDelete(ids);
    table.resetRowSelection();
  }

  return (
   <>
    <button 
      disabled={!isSelected} 
      onClick={handleDelete} 
    >削除</button>
    <Table>
      <TableHead>
        {table.getHeaderGroups().map((headerGroup) => (
          <TableRow key={headerGroup.id}>
            {headerGroup.headers.map((header) => (
              <TableCell key={header.id}>
                {flexRender(header.column.columnDef.header, header.getContext())}
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableHead>
      <TableBody>
        {table.getCoreRowModel().rows.map((row) => (
          <TableRow key={row.id}>
            {row.getVisibleCells().map((cell) => (
              <TableCell key={cell.id}>
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
   </>
  )
}

まとめ

tanstack tableについて、サンプルコードを交えて紹介させていただきました。
今回実装したチェックボックス以外に、ページネーションやソート機能も作成することができるので、詳しく知りたい方は、公式ドキュメントを見ていただければと思います。
長くなってしまいましたが、本記事をお読みいただきありがとうございました。

Copyright © RAKUS Co., Ltd. All rights reserved.