Reusable and Editable Table 구현하기
yarn add @tanstack/react-table
or
npm install @tanstack/react-table
const data: Person[] = [
{
firstName: 'tanner',
lastName: 'linsley',
age: 24,
visits: 100,
status: 'In Relationship',
progress: 50,
}
...
]
테이블에 그려질 데이터. 객체의 key가 column 의 accessorKey와 매칭된다.
const columns: ColumnDef<Person>[] = [
{
accessorKey: 'firstName', // necessary
id: 'FIRSTNAME', // optional
header: 'First Name', // optional
cell, footer, columns etc // optional
}
...
]
const columnHelper = createColumnHelper<Person>();
const columns = [
columnHelper.display({
id: 'actions',
cell: props => <RowActions row={props.row} />,
}),
columnHelper.group({
header: 'Name',
columns: [
columnHelper.accessor('firstName'),
columnHelper.accessor(row => row.lastName, {
header: () => <span>Last Name</span>,
}),
],
})
columnHelper.accessor('age', {
header: () => 'Age',
}),
...
];
// Table.tsx
...
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), });
return <table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
// Usage
<Table data={data} columns={columns} />
<th key={header.id} colSpan={header.colSpan} />
으로 해당 테이블 헤더의 colSpan을 입력해준다.const columns = [
{
accessorKey: 'firstName',
cell: ({ getValue, row, column, table }) => {
const initialValue = getValue();
const [value, setValue] = React.useState(initialValue);
const onBlur = () => {
table.options.meta?.updateData(row.index, column.id, value);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return <input value={value as string} onChange={e => setValue(e.target.value)} onBlur={onBlur} />
},
}
...
]
firstName 컬럼의 셀을 input 으로 변경했다.
셀 기본값을 input value state로 사용하였고, 수정 사항을 반영하기 위해 인풋 onBlur를 사용하여 업데이트 시켜주었다. 여기서의 table.options.meta 는 useReactTable 의 meta 옵션을 의미한다.
export interface TableMeta<TData extends RowData> {
}
패키지 타입선언 파일을 보면 meta 내부 형식은 비어져있다. 아무거나 정의해서 사용하면 될 것 같다.
useTable({
...
meta: {
test: () => console.log('test'),
}
}
위와 같이 작성하면 table.options.meta?.test()
로 사용이 가능하지만 test의 타입을 지정해 주지 않아서 타입에러가 발생한다.
declare module '@tanstack/react-table' {
interface TableMeta<TData extends RowData> {
test: () => void;
updateData: (rowIndex: number, columnId: string, value: unknown) => void;
}
}
tanstack-react-table-config.d.ts
같은 타입선언 파일로 따로 작성 하게되면, 패키지의 타입 전체가 이 파일로 덮어씌워지기 때문에 useTable 훅을 사용하는 파일 상단 부분에 정의해주자.
const updateData = (rowIndex: number, columnId: string, value: string) => {
const updatedData = data.map((row, index) => {
if (index === rowIndex) {
return {
...data[rowIndex],
[columnId]: value,
};
}
return row;
});
setData(updatedData);
};
interface ShowInputCondType {
rows?: string[];
columns?: string[];
}
cell: ({ getValue, row, column, table, renderValue }) => {
...
const showInput = shouldShowInput(row.id, column.id, showInputCond);
return showInput ? (
<input value={value as string} onChange={e => setValue(e.target.value)} onBlur={onBlur} />
) : (
renderValue()
);
}
function shouldShowInput(rowId: string, columnId: string, showInputCond?: ShowInputCondType): boolean {
if (!showInputCond) {
return false;
}
const { columns = [], rows = [] } = showInputCond;
return (rows.length === 0 || rows.includes(rowId)) && (columns.length === 0 || columns.includes(columnId));
}
input을 보여줄 row를 담은 배열 rows, column을 담은 배열 columns을 가진 showInputCond 가 있다.
interface ShowInputCondType {
columns?: string[];
rows?: string[];
}
function useTable<T extends object>(data: T[], showInputCond?: ShowInputCondType) {
const keysOfData = Object.keys(data[0]);
const [cpyData, setCpyData] = React.useState(data);
const rows = React.useMemo(() => data, [data]);
const columns = React.useMemo<ColumnDef<T>[]>(
() =>
keysOfData.map(key =>
Object.assign({
accessorKey: key,
header: startCase(key),
cell: ({ getValue, row, column, table, renderValue }) => {
const initialValue = getValue();
const [value, setValue] = React.useState(initialValue);
const onBlur = () => {
// table.options.meta?.updateData(row.index, column.id, value);
updateCpyData(row.index, column.id, value);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const showInput = shouldShowInput(row.id, column.id, showInputCond);
return showInput ? (
<input value={value as string} onChange={e => setValue(e.target.value)} onBlur={onBlur} />
) : (
renderValue()
);
},
} as ColumnDef<T>),
),
[data],
);
const updateCpyData = (rowIndex: number, columnId: string, value: string) => {
const updatedCpyData = cpyData.map((row, index) => {
if (index === rowIndex) {
return {
...cpyData[rowIndex],
[columnId]: value,
};
}
return row;
});
setCpyData(updatedCpyData);
};
if (!data || data.length === 0) return {};
return { cpyData, columns, rows };
}
function shouldShowInput(rowId: string, columnId: string, showInputCond?: ShowInputCondType): boolean {
if (!showInputCond) {
return false;
}
const { columns = [], rows = [] } = showInputCond;
return (rows.length === 0 || rows.includes(rowId)) && (columns.length === 0 || columns.includes(columnId));
}
export default useTable;
interface TableProps<T extends object> {
data: T[];
columns: ColumnDef<T>[];
}
function Table<T extends object>({ data, columns }: TableProps<T>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
...
}
export default Table;
const getData = async () => {
const res = await axios.get<Products>('https://dummyjson.com/products');
return res.data;
};
export default function TestPage() {
const { data } = useQuery<Products>(['Products'], getData);
const tableData = data?.products ?? [{} as Product];
const showInputCond = {
columns: ['description'],
rows: ['8', '9'],
};
const { cpyData, columns, rows } = useTable(tableData, showInputCond);
if (!data) return <Loading />
return (
<>
<Table<Product> columns={columns} data={rows} />
</>
);
}