MUIとカスタムフックで選択可能なテーブルを作る

2022/01/23に公開

はじめに

UIコンポーネントとしてMUIを使用して選択可能なテーブルを実装する際に、公式のサンプルが少しわかりずらかったので公式を参考にしつつ自前で実装したいと思います。

また行の選択に関するステートやロジックはカスタムフックで抽出して、汎用的に使えるようにしていきます。

https://mui.com/components/tables/#sorting-amp-selecting

最終的なコードはGithubにpushしてあるので、参考にしてみてください!

https://github.com/KazukiHayase/mui-selectable-table-sample

前提

  • React 17.0.2(Next.js 12.0.7)
  • MUI 5.2.6

コンポーネント作成

まずは公式を参考にしてテーブルヘッダーのコンポーネントを作成します。

公式のサンプルからソートに関する箇所を排除しただけで、ほとんど公式のサンプルと変わりはないです。

onSelectAllClickはテーブルのヘッダーにある全選択用のチェックボックスを押下した時のイベントハンドラを受け取ります。

SelectableTableHead.tsx
export type HeadCell = {
  id: string;
  label: string;
};

type SelectableTableHeadProps = {
  onSelectAllClick: (event: React.ChangeEvent<HTMLInputElement>) => void;
  headCells: HeadCell[];
  checked: boolean;
  indeterminate: boolean;
};

export const SelectableTableHead: React.VFC<SelectableTableHeadProps> = ({
  onSelectAllClick,
  headCells,
  checked,
  indeterminate,
}) => {
  return (
    <TableHead>
      <TableRow>
        <TableCell padding="checkbox">
          <Checkbox
            indeterminate={indeterminate}
            checked={checked}
            onChange={onSelectAllClick}
          />
        </TableCell>
        {headCells.map((headCell) => (
          <TableCell key={headCell.id} padding={"normal"}>
            {headCell.label}
          </TableCell>
        ))}
      </TableRow>
    </TableHead>
  );
};

カスタムフック作成

次に選択された行のステート管理や、行が選択された場合の挙動を制御するためのカスタムフックを作成していきます。

カスタムフックを呼び出す際に全ての行に一意なidが割り当てられている前提で、そのテーブルの全ての行のidを含んだ配列とすでに選択されている行のidの配列(オプション)を引数として受け取ります。

カスタムフック内で選択された行のidを配列として持つことでどの行が選択されているかを管理します。

カスタムフックが返すtoggleSelectedtoggleSelectedAllを使用することで、行が選択されているかの判定や行の選択・全選択のトグルを行うことができます。

useRowSelect.ts
export const useRowSelect = (
  rowIds: number[],
  initialSelectedRowIds: number[] = []
): {
  selectedRowIds: number[];
  isSelected: (rowId: number) => boolean;
  isSelectedAll: boolean;
  isIndeterminate: boolean;
  toggleSelected: (id: number) => void;
  toggleSelectedAll: () => void;
} => {
  const [selectedRowIds, setSelectedRowIds] = useState<number[]>(
    initialSelectedRowIds
  );

  const isSelected = (rowId: number) => selectedRowIds.includes(rowId);
  const isSelectedAll =
    rowIds.length > 0 && selectedRowIds.length === rowIds.length;
  const isIndeterminate =
    selectedRowIds.length > 0 && selectedRowIds.length < rowIds.length;

  const toggleSelected = (rowId: number) => {
    isSelected(rowId)
      ? setSelectedRowIds(
          selectedRowIds.filter((selectedId) => selectedId !== rowId)
        )
      : setSelectedRowIds([...selectedRowIds, rowId]);
  };
  const toggleSelectedAll = () => {
    isSelectedAll ? setSelectedRowIds([]) : setSelectedRowIds(rowIds);
  };

  return {
    selectedRowIds,
    isSelected,
    isSelectedAll,
    isIndeterminate,
    toggleSelected,
    toggleSelectedAll,
  };
};

つなぎこみ

最後に上記で作成したものを使用してつなぎこみを行えば完成です!

カスタムフックを呼び出し先ほど作成したテーブルヘッドコンポーネントにtoggleSelectedAllを渡し、テーブルの行コンポーネントにisSelected(row.id)toggleSelected(row.id)を渡すことで各行の挙動を管理できるようになります。

index.tsx
const IndexPage: NextPage = () => {
  const {
    selectedRowIds,
    isSelected,
    isSelectedAll,
    isIndeterminate,
    toggleSelected,
    toggleSelectedAll,
  } = useRowSelect(rows.map((row) => row.id));

  return (
    <Box>
      <Typography>
        Selectable Table Sample
      </Typography>
      <TableContainer component={Paper}>
        <Table>
          <SelectableTableHead
            onSelectAllClick={toggleSelectedAll}
            headCells={headCells}
            checked={isSelectedAll}
            indeterminate={isIndeterminate}
          />
          <TableBody>
            {rows.map((row) => {
              const isItemSelected = isSelected(row.id);

              return (
                <TableRow
                  hover
                  role="checkbox"
                  tabIndex={-1}
                  key={row.id}
                  onClick={() => toggleSelected(row.id)}
                  selected={isItemSelected}
                >
                  <TableCell padding="checkbox">
                    <Checkbox checked={isItemSelected} />
                  </TableCell>
                  <TableCell>{row.name}</TableCell>
                  <TableCell>{row.calories}</TableCell>
                  <TableCell>{row.fat}</TableCell>
                  <TableCell>{row.carbs}</TableCell>
                  <TableCell>{row.protein}</TableCell>
                </TableRow>
              );
            })}
          </TableBody>
        </Table>
      </TableContainer>
      <Box>
        <Typography>selectedRowIds</Typography>
        <Typography>{JSON.stringify(selectedRowIds)}</Typography>
      </Box>
    </Box>
  );
};

下記がコンポーネントに渡してるデータになります。(ほぼMUIのサンプルそのまま)

type Data = {
  id: number;
  calories: number;
  carbs: number;
  fat: number;
  name: string;
  protein: number;
};

const createData = (
  id: number,
  name: string,
  calories: number,
  fat: number,
  carbs: number,
  protein: number
): Data => {
  return {
    id,
    name,
    calories,
    fat,
    carbs,
    protein,
  };
};

const rows = [
  createData(1, "Cupcake", 305, 3.7, 67, 4.3),
  createData(2, "Donut", 452, 25.0, 51, 4.9),
  createData(3, "Eclair", 262, 16.0, 24, 6.0),
  createData(4, "Frozen yoghurt", 159, 6.0, 24, 4.0),
  createData(5, "Gingerbread", 356, 16.0, 49, 3.9),
  createData(6, "Honeycomb", 408, 3.2, 87, 6.5),
  createData(7, "Ice cream sandwich", 237, 9.0, 37, 4.3),
  createData(8, "Jelly Bean", 375, 0.0, 94, 0.0),
  createData(9, "KitKat", 518, 26.0, 65, 7.0),
  createData(10, "Lollipop", 392, 0.2, 98, 0.0),
  createData(11, "Marshmallow", 318, 0, 81, 2.0),
  createData(12, "Nougat", 360, 19.0, 9, 37.0),
  createData(13, "Oreo", 437, 18.0, 63, 4.0),
];

デモ

最終的に完成したテーブルの挙動は下記のようになります。

まとめ

選択された行idの配列を元にAPIリクエストを行ったりすることで、選択した行に対する一括操作ができるようになります。

またApollo Clientを使用してソートやページネーションを含めた包括的なテーブルもMUIで実装したりしているので、気が向けばそちらも記事にしたいと思います!

株式会社BuySell Technologies

Discussion