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

# 公式のサンプルはシンプル
以下のリンクの通り、React Tableとの組み合わせ例をChakra UI公式で掲載してくれている。
しかし、表示するだけのシンプルな機能のため、この機能だけならReact Tableを使う必要性は薄い。
[ChakraUI公式のReact Table組み合わせ例](https://chakra-ui.com/getting-started/with-react-table)

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

# 完成系プレビュー
![chakra-editable-table.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647020/1022dcda-7154-fcd7-e945-6032d4f20617.png)


# 編集可能なテーブルに変えてみる
[React Table公式のeditable table](https://tanstack.com/table/v8/docs/examples/react/editable-data)を参考に実装していく。
CompanyInfo型は今回テーブルで扱うレコードの型で勝手につけたもの。
ライブラリの性質上、UIと機能(React Table)は独立しており使い勝手は良い。

```TypeScript: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のものへ変更している。

```TypeScript: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://codesandbox.io/embed/chakra-editable-table-hncuus?fontsize=14&hidenavigation=1&theme=dark

# 参考
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