3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

公式のChakra UI+React Tableを参考にeditableで実装

Last updated at Posted at 2022-10-15

公式のサンプルはシンプル

以下のリンクの通り、React Tableとの組み合わせ例をChakra UI公式で掲載してくれている。
しかし、表示するだけのシンプルな機能のため、この機能だけならReact Tableを使う必要性は薄い。
ChakraUI公式のReact Table組み合わせ例

そこで、フィルター・編集機能を追加して実装してみる。

完成系プレビュー

chakra-editable-table.PNG

編集可能なテーブルに変えてみる

React Table公式のeditable tableを参考に実装していく。
CompanyInfo型は今回テーブルで扱うレコードの型で勝手につけたもの。
ライブラリの性質上、UIと機能(React Table)は独立しており使い勝手は良い。

Table.tsxのテーブル(Chakra UI)へ渡すデータ周り
// editableにするため
declare module "@tanstack/react-table" {
  interface TableMeta<TData extends RowData> {
    updateData: (rowIndex: number, columnId: string, value: unknown) => void;
  }
}

type CompanyInfo = {
  name: string;
  field: string;
  shareRate: number;
};

type Props = {};

// Give our default column cell renderer editing superpowers!
const defaultColumn: Partial<ColumnDef<CompanyInfo>> = {
  cell: ({ getValue, row: { index }, column: { id }, table }) => {
    const initialValue = getValue();
    // We need to keep and update the state of the cell normally
    const [value, setValue] = useState(initialValue);

    // When the input is blurred, we'll call our table meta's updateData function
    const onBlur = () => {
      table.options.meta?.updateData(index, id, value);
    };

    // If the initialValue is changed external, sync it up with our state
    useEffect(() => {
      setValue(initialValue);
    }, [initialValue]);

    return (
      <input
        value={value as string}
        onChange={(e) => setValue(e.target.value)}
        onBlur={onBlur}
      />
    );
  }
};
// pagingカスタムフック
const useSkipper = () => {
  const shouldSkipRef = useRef(true);
  const shouldSkip = shouldSkipRef.current;

  // Wrap a function with this to skip a pagination reset temporarily
  const skip = useCallback(() => {
    shouldSkipRef.current = false;
  }, []);

  useEffect(() => {
    shouldSkipRef.current = true;
  });

  return [shouldSkip, skip] as const;
};
// フィルターコンポーネント
const Filter = ({
  column,
  table
}: {
  column: Column<any, any>;
  table: Table<any>;
}) => {
  const firstValue = table
    .getPreFilteredRowModel()
    .flatRows[0]?.getValue(column.id);

  const columnFilterValue = column.getFilterValue();

  return typeof firstValue === "number" ? (
    <div>
      <Input
        type="number"
        value={(columnFilterValue as [number, number])?.[0] ?? ""}
        onChange={(e) =>
          column.setFilterValue((old: [number, number]) => [
            e.target.value,
            old?.[1]
          ])
        }
        placeholder={`Min`}
      />
      <Input
        type="number"
        value={(columnFilterValue as [number, number])?.[1] ?? ""}
        onChange={(e) =>
          column.setFilterValue((old: [number, number]) => [
            old?.[0],
            e.target.value
          ])
        }
        placeholder={`Max`}
      />
    </div>
  ) : (
    <Input
      type="text"
      value={(columnFilterValue ?? "") as string}
      onChange={(e) => column.setFilterValue(e.target.value)}
      placeholder={`Search...`}
    />
  );
};

Chakra UIがReact Tableから受け取っている部分は以下。
せっかくなのでアイコンをChakra UIのものへ変更している。

Table.tsx UI部分

/**
 * Chakra-UIとreact-tableを用いた編集・フィルター可能テーブル
 */
const SampleTable = (props: Props) => {
  const rerender = useReducer(() => ({}), {})[1];

  // カラム定義
  const columns = useMemo<Array<ColumnDef<CompanyInfo>>>(
    () => [
      {
        header: "半導体関連企業",
        footer: (props) => props.column.id,
        columns: [
          {
            accessorKey: "name",
            header: () => <span>企業名</span>,
            footer: (props) => props.column.id
          },
          {
            accessorKey: "field",
            header: () => <span>分野</span>,
            footer: (props) => props.column.id
          },
          {
            accessorKey: "shareRate",
            header: () => <span>世界シェア</span>,
            footer: (props) => props.column.id
          }
        ]
      }
    ],
    []
  );
  // テーブルに渡すデータ(とりあえず固定値)
  const [data, setData] = useState<CompanyInfo[]>([
    { name: "東京エレクトロン", field: "コータ/デベロッパ", shareRate: 90 },
    { name: "信越化学工業", field: "シリコンウエハ", shareRate: 30 }
  ]);

  const [autoResetPageIndex, skipAutoResetPageIndex] = useSkipper();

  const table = useReactTable({
    data,
    columns,
    defaultColumn,
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    autoResetPageIndex,
    // Provide our updateData function to our table meta
    meta: {
      updateData: (rowIndex, columnId, value) => {
        // Skip age index reset until after next rerender
        skipAutoResetPageIndex();
        setData((old) =>
          old.map((row, index) => {
            if (index === rowIndex) {
              return {
                ...old[rowIndex]!,
                [columnId]: value
              };
            }
            return row;
          })
        );
      }
    },
    debugTable: true
  });
  return (
    <ChakraProvider>
      <div>
        <div />
        <ChakraTable>
          <Thead>
            {table.getHeaderGroups().map((headerGroup) => (
              <Tr key={headerGroup.id}>
                {headerGroup.headers.map((header) => {
                  return (
                    <Th key={header.id} colSpan={header.colSpan}>
                      {header.isPlaceholder ? null : (
                        <div>
                          {flexRender(
                            header.column.columnDef.header,
                            header.getContext()
                          )}
                          {header.column.getCanFilter() ? (
                            <div>
                              <Filter column={header.column} table={table} />
                            </div>
                          ) : null}
                        </div>
                      )}
                    </Th>
                  );
                })}
              </Tr>
            ))}
          </Thead>
          <Tbody>
            {table.getRowModel().rows.map((row) => {
              return (
                <Tr key={row.id}>
                  {row.getVisibleCells().map((cell) => {
                    return (
                      <Td key={cell.id}>
                        {flexRender(
                          cell.column.columnDef.cell,
                          cell.getContext()
                        )}
                      </Td>
                    );
                  })}
                </Tr>
              );
            })}
          </Tbody>
        </ChakraTable>
        <div />
        <div>
          <Box>
            <IconButton
              onClick={() => table.setPageIndex(0)}
              disabled={!table.getCanPreviousPage()}
              aria-label="jumpFirst"
              icon={<ArrowLeftIcon />}
            />

            <IconButton
              onClick={() => table.previousPage()}
              disabled={!table.getCanPreviousPage()}
              aria-label="back1"
              icon={<ChevronLeftIcon />}
            />

            <IconButton
              onClick={() => table.nextPage()}
              disabled={!table.getCanNextPage()}
              aria-label="next1"
              icon={<ChevronRightIcon />}
            />

            <IconButton
              onClick={() => table.setPageIndex(table.getPageCount() - 1)}
              disabled={!table.getCanNextPage()}
              aria-label="jumpFinish"
              icon={<ArrowRightIcon />}
            />
          </Box>
          <span>
            <div>Page</div>
            <strong>
              {table.getState().pagination.pageIndex + 1} of{" "}
              {table.getPageCount()}
            </strong>
          </span>
          <span>
            | Go to page:
            <input
              type="number"
              defaultValue={table.getState().pagination.pageIndex + 1}
              onChange={(e) => {
                const page = e.target.value ? Number(e.target.value) - 1 : 0;
                table.setPageIndex(page);
              }}
            />
          </span>
          <select
            value={table.getState().pagination.pageSize}
            onChange={(e) => {
              table.setPageSize(Number(e.target.value));
            }}
          >
            {[1, 10, 20, 30, 40, 50].map((pageSize) => (
              <option key={pageSize} value={pageSize}>
                Show {pageSize}
              </option>
            ))}
          </select>
        </div>
        <div>{table.getRowModel().rows.length} Rows</div>
        <div>
          <button onClick={() => rerender()}>Force Rerender</button>
        </div>
      </div>
    </ChakraProvider>
  );
};

感想

React Tableが自身の責務のみ担うスタンスのため、UIライブラリに相性良いと感じた。
余談だが、Chakra UIはProviderで囲ってあげないとデフォルトのスタイルも当たらないようだ。(たしかMUIはthemeを渡さなくてもスタイルが当たっていたはず)

全てのコード

参考

https://chakra-ui.com/getting-started/with-react-table
https://tanstack.com/table/v8/docs/examples/react/editable-data
https://zenn.dev/slowhand/articles/65f1baf9869116
https://qiita.com/kurab/items/93c56277451e96f2b1eb

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?