테이블
테이블 (Table)
구조화된 데이터를 행과 열로 정렬된 테이블로 보여주고, 정렬·검색·페이징·필터까지 지원하는 강력한 컴포넌트입니다.
출처: 문서
본문
언제 사용하나요 (When To Use)
- 구조화된 데이터의 집합을 표시할 때
- 데이터를 정렬, 검색, 페이징, 필터링할 때
사용 방법 (How To Use)
Table의 dataSource를 데이터 배열로 지정합니다.
const dataSource = [
{
key: '1',
name: 'Mike',
age: 32,
address: '10 Downing Street',
},
{
key: '2',
name: 'John',
age: 42,
address: '10 Downing Street',
},
];
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
},
];
<Table dataSource={dataSource} columns={columns} />;
추천 자료 (Promotion)
예제 (Examples)
기본 사용법 (Basic Usage)
액션이 있는 간단한 테이블입니다.
import React from 'react';
import { Flex, Space, Table, Tag } from 'antd';
import type { TableProps } from 'antd';
interface DataType {
key: string;
name: string;
age: number;
address: string;
tags: string[];
}
const columns: TableProps<DataType>['columns'] = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (text) => <a>{text}</a>,
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
},
{
title: 'Tags',
key: 'tags',
dataIndex: 'tags',
render: (_, { tags }) => (
<Flex gap="small" align="center" wrap>
{tags.map((tag) => {
let color = tag.length > 5 ? 'geekblue' : 'green';
if (tag === 'kawaii') {
color = 'volcano';
}
return (
<Tag color={color} key={tag}>
{tag.toUpperCase()}
</Tag>
);
})}
</Flex>
),
},
{
title: 'Action',
key: 'action',
render: (_, record) => (
<Space size="medium">
<a>Invite {record.name}</a>
<a>Delete</a>
</Space>
),
},
];
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer'],
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['kawaii'],
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
tags: ['cool', 'teacher'],
},
];
const App: React.FC = () => <Table<DataType> columns={columns} dataSource={data} />;
export default App;
JSX 스타일 API (JSX style API)
JSX 스타일 API를 사용합니다(2.5.0에서 도입).
이것은 prop
columns의 문법적 설탕(syntax sugar)일 뿐이므로,Column과ColumnGroup을 다른 컴포넌트와 조합할 수 없습니다.
import React from 'react';
import { Flex, Space, Table, Tag } from 'antd';
const { Column, ColumnGroup } = Table;
interface DataType {
key: React.Key;
firstName: string;
lastName: string;
age: number;
address: string;
tags: string[];
}
const data: DataType[] = [
{
key: '1',
firstName: 'John',
lastName: 'Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer'],
},
{
key: '2',
firstName: 'Jim',
lastName: 'Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['kawaii'],
},
{
key: '3',
firstName: 'Joe',
lastName: 'Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
tags: ['cool', 'teacher'],
},
];
const App: React.FC = () => (
<Table<DataType> dataSource={data}>
<ColumnGroup title="Name">
<Column title="First Name" dataIndex="firstName" key="firstName" />
<Column title="Last Name" dataIndex="lastName" key="lastName" />
</ColumnGroup>
<Column title="Age" dataIndex="age" key="age" />
<Column title="Address" dataIndex="address" key="address" />
<Column
title="Tags"
dataIndex="tags"
key="tags"
render={(tags: string[]) => (
<Flex gap="small" align="center" wrap>
{tags.map((tag) => {
let color = tag.length > 5 ? 'geekblue' : 'green';
if (tag === 'kawaii') {
color = 'volcano';
}
return (
<Tag color={color} key={tag}>
{tag.toUpperCase()}
</Tag>
);
})}
</Flex>
)}
/>
<Column
title="Action"
key="action"
render={(_: any, record: DataType) => (
<Space size="medium">
<a>Invite {record.lastName}</a>
<a>Delete</a>
</Space>
)}
/>
</Table>
);
export default App;
선택 (selection)
첫 번째 컬럼을 선택 가능한 컬럼으로 만들어 행을 선택할 수 있습니다. rowSelection.type으로 선택 타입을 설정합니다. 기본값은 checkbox입니다.
기본적으로 체크박스를 클릭할 때 선택이 발생합니다. 행 클릭 선택 동작이 필요하다면 https://codesandbox.io/s/000vqw38rl를 참고하세요.
import React, { useState } from 'react';
import { Divider, Radio, Table } from 'antd';
import type { TableColumnsType, TableProps } from 'antd';
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
render: (text: string) => <a>{text}</a>,
},
{
title: 'Age',
dataIndex: 'age',
},
{
title: 'Address',
dataIndex: 'address',
},
];
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
},
{
key: '4',
name: 'Disabled User',
age: 99,
address: 'Sydney No. 1 Lake Park',
},
];
// rowSelection object indicates the need for row selection
const rowSelection: TableProps<DataType>['rowSelection'] = {
onChange: (selectedRowKeys: React.Key[], selectedRows: DataType[]) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record: DataType) => ({
disabled: record.name === 'Disabled User', // Column configuration not to be checked
name: record.name,
}),
};
const App: React.FC = () => {
const [selectionType, setSelectionType] = useState<'checkbox' | 'radio'>('checkbox');
return (
<div>
<Radio.Group onChange={(e) => setSelectionType(e.target.value)} value={selectionType}>
<Radio value="checkbox">Checkbox</Radio>
<Radio value="radio">radio</Radio>
</Radio.Group>
<Divider />
<Table<DataType>
rowSelection={{ type: selectionType, ...rowSelection }}
columns={columns}
dataSource={data}
/>
</div>
);
};
export default App;
선택과 작업 (Selection and operation)
일부 행을 선택한 뒤 작업을 수행하고 선택을 해제하려면 rowSelection.selectedRowKeys로 선택된 행을 제어합니다.
import React, { useState } from 'react';
import { Button, Flex, Table } from 'antd';
import type { TableColumnsType, TableProps } from 'antd';
type TableRowSelection<T extends object = object> = TableProps<T>['rowSelection'];
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{ title: 'Name', dataIndex: 'name' },
{ title: 'Age', dataIndex: 'age' },
{ title: 'Address', dataIndex: 'address' },
];
const dataSource = Array.from<DataType>({ length: 46 }).map<DataType>((_, i) => ({
key: i,
name: `Edward King ${i}`,
age: 32,
address: `London, Park Lane no. ${i}`,
}));
const App: React.FC = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [loading, setLoading] = useState(false);
const start = () => {
setLoading(true);
// ajax request after empty completing
setTimeout(() => {
setSelectedRowKeys([]);
setLoading(false);
}, 1000);
};
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys changed: ', newSelectedRowKeys);
setSelectedRowKeys(newSelectedRowKeys);
};
const rowSelection: TableRowSelection<DataType> = {
selectedRowKeys,
onChange: onSelectChange,
};
const hasSelected = selectedRowKeys.length > 0;
return (
<Flex gap="medium" vertical>
<Flex align="center" gap="medium">
<Button type="primary" onClick={start} disabled={!hasSelected} loading={loading}>
Reload
</Button>
{hasSelected ? `Selected ${selectedRowKeys.length} items` : null}
</Flex>
<Table<DataType> rowSelection={rowSelection} columns={columns} dataSource={dataSource} />
</Flex>
);
};
export default App;
커스텀 선택 (Custom selection)
rowSelection.selections로 커스텀 선택을 사용합니다. 기본적으로 선택 드롭다운이 없으며, true로 설정하면 기본 선택이 표시됩니다.
import React, { useState } from 'react';
import { Table } from 'antd';
import type { TableColumnsType, TableProps } from 'antd';
type TableRowSelection<T extends object = object> = TableProps<T>['rowSelection'];
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Age',
dataIndex: 'age',
},
{
title: 'Address',
dataIndex: 'address',
},
];
const dataSource = Array.from({ length: 46 }).map<DataType>((_, i) => ({
key: i,
name: `Edward King ${i}`,
age: 32,
address: `London, Park Lane no. ${i}`,
}));
const App: React.FC = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys changed: ', newSelectedRowKeys);
setSelectedRowKeys(newSelectedRowKeys);
};
const rowSelection: TableRowSelection<DataType> = {
selectedRowKeys,
onChange: onSelectChange,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE,
{
key: 'odd',
text: 'Select Odd Row',
onSelect: (changeableRowKeys) => {
setSelectedRowKeys(changeableRowKeys.filter((_, index) => index % 2 === 0));
},
},
{
key: 'even',
text: 'Select Even Row',
onSelect: (changeableRowKeys) => {
setSelectedRowKeys(changeableRowKeys.filter((_, index) => index % 2 !== 0));
},
},
],
};
return <Table<DataType> rowSelection={rowSelection} columns={columns} dataSource={dataSource} />;
};
export default App;
필터와 정렬 (Filter and sorter)
filters로 컬럼에 필터 메뉴를 생성하고, onFilter로 필터 결과를 결정하며, filterMultiple로 다중/단일 선택 여부를, filterOnClose로 필터 메뉴가 닫힐 때 필터를 트리거할지 지정합니다.
defaultFilteredValue로 컬럼이 기본적으로 필터링되도록 만들 수 있습니다.
sorter로 컬럼을 정렬 가능하게 만듭니다. sorter는 sorter: function(rowA, rowB) { ... } 형태의 함수가 될 수 있고, 데이터를 로컬에서 정렬합니다.
sortDirections: ['ascend', 'descend']는 각 컬럼에 사용 가능한 정렬 방법을 정의합니다. 테이블 props에 설정하면 모든 컬럼에 적용됩니다. ['ascend', 'descend', 'ascend']로 설정해 정렬이 기본 상태로 되돌아가지 않게 할 수 있습니다.
defaultSortOrder로 컬럼이 기본적으로 정렬되도록 만들 수 있습니다.
sortOrder 또는 defaultSortOrder가 ascend나 descend 값으로 지정되면, 위에서 설명한 대로 sorter에 전달된 함수 내부에서 이 값을 접근할 수 있습니다. 그런 함수는 function(a, b, sortOrder) { ... } 형태를 가질 수 있습니다.
import React from 'react';
import { Table } from 'antd';
import type { TableColumnsType, TableProps } from 'antd';
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
showSorterTooltip: { target: 'full-header' },
filters: [
{
text: 'Joe',
value: 'Joe',
},
{
text: 'Jim',
value: 'Jim',
},
{
text: 'Submenu',
value: 'Submenu',
children: [
{
text: 'Green',
value: 'Green',
},
{
text: 'Black',
value: 'Black',
},
],
},
],
// specify the condition of filtering result
// here is that finding the name started with `value`
onFilter: (value, record) => record.name.indexOf(value as string) === 0,
sorter: (a, b) => a.name.length - b.name.length,
sortDirections: ['descend'],
},
{
title: 'Age',
dataIndex: 'age',
defaultSortOrder: 'descend',
sorter: (a, b) => a.age - b.age,
},
{
title: 'Address',
dataIndex: 'address',
filters: [
{
text: 'London',
value: 'London',
},
{
text: 'New York',
value: 'New York',
},
],
onFilter: (value, record) => record.address.indexOf(value as string) === 0,
},
];
const data = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
},
{
key: '4',
name: 'Jim Red',
age: 32,
address: 'London No. 2 Lake Park',
},
];
const onChange: TableProps<DataType>['onChange'] = (pagination, filters, sorter, extra) => {
console.log('params', pagination, filters, sorter, extra);
};
const App: React.FC = () => (
<Table<DataType>
columns={columns}
dataSource={data}
onChange={onChange}
showSorterTooltip={{ target: 'sorter-icon' }}
/>
);
export default App;
트리 필터 (Filter in Tree)
filterMode로 기본 필터 인터페이스를 변경할 수 있습니다. 옵션은 menu(기본값)와 tree입니다.
filterSearch는 필터 드롭다운 항목을 검색 가능하게 만드는 데 사용됩니다.
import React from 'react';
import { Table } from 'antd';
import type { TableColumnsType, TableProps } from 'antd';
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
filters: [
{
text: 'Joe',
value: 'Joe',
},
{
text: 'Category 1',
value: 'Category 1',
children: [
{
text: 'Yellow',
value: 'Yellow',
},
{
text: 'Pink',
value: 'Pink',
},
],
},
{
text: 'Category 2',
value: 'Category 2',
children: [
{
text: 'Green',
value: 'Green',
},
{
text: 'Black',
value: 'Black',
},
],
},
],
filterMode: 'tree',
filterSearch: true,
onFilter: (value, record) => record.name.includes(value as string),
width: '30%',
},
{
title: 'Age',
dataIndex: 'age',
sorter: (a, b) => a.age - b.age,
},
{
title: 'Address',
dataIndex: 'address',
filters: [
{
text: 'London',
value: 'London',
},
{
text: 'New York',
value: 'New York',
},
],
onFilter: (value, record) => record.address.startsWith(value as string),
filterSearch: true,
width: '40%',
},
];
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
},
{
key: '4',
name: 'Jim Red',
age: 32,
address: 'London No. 2 Lake Park',
},
];
const onChange: TableProps<DataType>['onChange'] = (pagination, filters, sorter, extra) => {
console.log('params', pagination, filters, sorter, extra);
};
const App: React.FC = () => (
<Table<DataType> columns={columns} dataSource={data} onChange={onChange} />
);
export default App;
필터 검색 (Filter search)
filterSearch로 필터 항목 검색을 활성화하고, filterSearch:(input, record) => boolean으로 커스텀 필터 메서드를 설정할 수 있습니다.
import React from 'react';
import { Table } from 'antd';
import type { TableColumnsType, TableProps } from 'antd';
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
filters: [
{
text: 'Joe',
value: 'Joe',
},
{
text: 'Category 1',
value: 'Category 1',
},
{
text: 'Category 2',
value: 'Category 2',
},
],
filterMode: 'tree',
filterSearch: true,
onFilter: (value, record) => record.name.startsWith(value as string),
width: '30%',
},
{
title: 'Age',
dataIndex: 'age',
sorter: (a, b) => a.age - b.age,
},
{
title: 'Address',
dataIndex: 'address',
filters: [
{
text: 'London',
value: 'London',
},
{
text: 'New York',
value: 'New York',
},
],
onFilter: (value, record) => record.address.startsWith(value as string),
filterSearch: true,
width: '40%',
},
];
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
},
{
key: '4',
name: 'Jim Red',
age: 32,
address: 'London No. 2 Lake Park',
},
];
const onChange: TableProps<DataType>['onChange'] = (pagination, filters, sorter, extra) => {
console.log('params', pagination, filters, sorter, extra);
};
const App: React.FC = () => (
<Table<DataType> columns={columns} dataSource={data} onChange={onChange} />
);
export default App;
다중 정렬 (Multiple sorter)
column.sorter는 multiple을 지원해 정렬 컬럼의 우선순위를 구성합니다. sorter.compare로 비교 함수를 커스터마이즈할 수 있고, 비워두면 상호작용만 사용할 수도 있습니다.
import React from 'react';
import { Table } from 'antd';
import type { TableColumnsType, TableProps } from 'antd';
interface DataType {
key: React.Key;
name: string;
chinese: number;
math: number;
english: number;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Chinese Score',
dataIndex: 'chinese',
sorter: {
compare: (a, b) => a.chinese - b.chinese,
multiple: 3,
},
},
{
title: 'Math Score',
dataIndex: 'math',
sorter: {
compare: (a, b) => a.math - b.math,
multiple: 2,
},
},
{
title: 'English Score',
dataIndex: 'english',
sorter: {
compare: (a, b) => a.english - b.english,
multiple: 1,
},
},
];
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
chinese: 98,
math: 60,
english: 70,
},
{
key: '2',
name: 'Jim Green',
chinese: 98,
math: 66,
english: 89,
},
{
key: '3',
name: 'Joe Black',
chinese: 98,
math: 90,
english: 70,
},
{
key: '4',
name: 'Jim Red',
chinese: 88,
math: 99,
english: 89,
},
];
const onChange: TableProps<DataType>['onChange'] = (pagination, filters, sorter, extra) => {
console.log('params', pagination, filters, sorter, extra);
};
const App: React.FC = () => (
<Table<DataType> columns={columns} dataSource={data} onChange={onChange} />
);
export default App;
필터와 정렬 초기화 (Reset filters and sorters)
filteredValue와 sortOrder로 필터와 정렬을 제어합니다.
filteredValue또는sortOrder를 정의하는 것은 제어(controlled) 모드에 있다는 뜻입니다.sortOrder는 오직 한 컬럼에만 할당해야 합니다.column.key가 필요합니다.
import React, { useState } from 'react';
import type { TableColumnsType, TableProps } from 'antd';
import { Button, Space, Table } from 'antd';
type OnChange = NonNullable<TableProps<DataType>['onChange']>;
type Filters = Parameters<OnChange>[1];
type GetSingle<T> = T extends (infer U)[] ? U : never;
type Sorts = GetSingle<Parameters<OnChange>[2]>;
interface DataType {
key: string;
name: string;
age: number;
address: string;
}
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
},
{
key: '4',
name: 'Jim Red',
age: 32,
address: 'London No. 2 Lake Park',
},
];
const App: React.FC = () => {
const [filteredInfo, setFilteredInfo] = useState<Filters>({});
const [sortedInfo, setSortedInfo] = useState<Sorts>({});
const handleChange: OnChange = (pagination, filters, sorter) => {
console.log('Various parameters', pagination, filters, sorter);
setFilteredInfo(filters);
setSortedInfo(sorter as Sorts);
};
const clearFilters = () => {
setFilteredInfo({});
};
const clearAll = () => {
setFilteredInfo({});
setSortedInfo({});
};
const setAgeSort = () => {
setSortedInfo({
order: 'descend',
columnKey: 'age',
});
};
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
filters: [
{ text: 'Joe', value: 'Joe' },
{ text: 'Jim', value: 'Jim' },
],
filteredValue: filteredInfo.name || null,
onFilter: (value, record) => record.name.includes(value as string),
sorter: (a, b) => a.name.length - b.name.length,
sortOrder: sortedInfo.columnKey === 'name' ? sortedInfo.order : null,
ellipsis: true,
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
sorter: (a, b) => a.age - b.age,
sortOrder: sortedInfo.columnKey === 'age' ? sortedInfo.order : null,
ellipsis: true,
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
filters: [
{ text: 'London', value: 'London' },
{ text: 'New York', value: 'New York' },
],
filteredValue: filteredInfo.address || null,
onFilter: (value, record) => record.address.includes(value as string),
sorter: (a, b) => a.address.length - b.address.length,
sortOrder: sortedInfo.columnKey === 'address' ? sortedInfo.order : null,
ellipsis: true,
},
];
return (
<>
<Space style={{ marginBottom: 16 }}>
<Button onClick={setAgeSort}>Sort age</Button>
<Button onClick={clearFilters}>Clear filters</Button>
<Button onClick={clearAll}>Clear filters and sorters</Button>
</Space>
<Table<DataType> columns={columns} dataSource={data} onChange={handleChange} />
</>
);
};
export default App;
커스텀 필터 패널 (Customized filter panel)
filterDropdown으로 커스텀 컬럼 검색 예제를 구현합니다.
clearFilters 함수에 boolean 타입 매개변수 closeDropdown을 추가합니다. 필터 메뉴를 닫을지는 기본적으로 true입니다. 필터링 중 옵션을 제출할지 여부를 정하는 confirm boolean 타입 매개변수를 추가합니다. 기본값은 true입니다.
import React, { useRef, useState } from 'react';
import { SearchOutlined } from '@ant-design/icons';
import type { InputRef, TableColumnsType, TableColumnType } from 'antd';
import { Button, Input, Space, Table } from 'antd';
import type { FilterDropdownProps } from 'antd/es/table/interface';
import Highlighter from 'react-highlight-words';
interface DataType {
key: string;
name: string;
age: number;
address: string;
}
type DataIndex = keyof DataType;
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Joe Black',
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Jim Green',
age: 32,
address: 'Sydney No. 1 Lake Park',
},
{
key: '4',
name: 'Jim Red',
age: 32,
address: 'London No. 2 Lake Park',
},
];
const App: React.FC = () => {
const [searchText, setSearchText] = useState('');
const [searchedColumn, setSearchedColumn] = useState('');
const searchInput = useRef<InputRef>(null);
const handleSearch = (
selectedKeys: string[],
confirm: FilterDropdownProps['confirm'],
dataIndex: DataIndex,
) => {
confirm();
setSearchText(selectedKeys[0]);
setSearchedColumn(dataIndex);
};
const handleReset = (clearFilters: () => void) => {
clearFilters();
setSearchText('');
};
const getColumnSearchProps = (dataIndex: DataIndex): TableColumnType<DataType> => ({
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => (
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}>
<Input
ref={searchInput}
placeholder={`Search ${dataIndex}`}
value={selectedKeys[0]}
onChange={(e) => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => handleSearch(selectedKeys as string[], confirm, dataIndex)}
style={{ marginBottom: 8, display: 'block' }}
/>
<Space>
<Button
type="primary"
onClick={() => handleSearch(selectedKeys as string[], confirm, dataIndex)}
icon={<SearchOutlined />}
size="small"
style={{ width: 90 }}
>
Search
</Button>
<Button
onClick={() => clearFilters && handleReset(clearFilters)}
size="small"
style={{ width: 90 }}
>
Reset
</Button>
<Button
type="link"
size="small"
onClick={() => {
confirm({ closeDropdown: false });
setSearchText((selectedKeys as string[])[0]);
setSearchedColumn(dataIndex);
}}
>
Filter
</Button>
<Button
type="link"
size="small"
onClick={() => {
close();
}}
>
close
</Button>
</Space>
</div>
),
filterIcon: (filtered: boolean) => (
<SearchOutlined style={{ color: filtered ? '#1677ff' : undefined }} />
),
onFilter: (value, record) =>
record[dataIndex]
.toString()
.toLowerCase()
.includes((value as string).toLowerCase()),
filterDropdownProps: {
onOpenChange(open) {
if (open) {
setTimeout(() => searchInput.current?.select(), 100);
}
},
},
render: (text) =>
searchedColumn === dataIndex ? (
<Highlighter
highlightStyle={{ backgroundColor: '#ffc069', padding: 0 }}
searchWords={[searchText]}
autoEscape
textToHighlight={text ? text.toString() : ''}
/>
) : (
text
),
});
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: '30%',
...getColumnSearchProps('name'),
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
width: '20%',
...getColumnSearchProps('age'),
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
...getColumnSearchProps('address'),
sorter: (a, b) => a.address.length - b.address.length,
sortDirections: ['descend', 'ascend'],
},
];
return <Table<DataType> columns={columns} dataSource={data} />;
};
export default App;
Ajax
이 예제는 원격 서버에서 데이터를 가져와 표시하는 방법, 그리고 서버 측 필터링·정렬을 위해 관련 매개변수를 서버로 보내는 방법을 보여줍니다.
선택을 활성화할 때 key를 유지하려면 rowSelection.preserveSelectedRowKeys를 설정합니다.
참고, 이 예제는 Mock API를 사용합니다. Network Console에서 확인할 수 있습니다.
import React, { useEffect, useState } from 'react';
import type { GetProp, TableProps } from 'antd';
import { Table } from 'antd';
import type { SorterResult } from 'antd/es/table/interface';
type ColumnsType<T extends object = object> = TableProps<T>['columns'];
type TablePaginationConfig = Exclude<GetProp<TableProps, 'pagination'>, boolean>;
interface DataType {
name: string;
gender: string;
email: string;
id: string;
}
interface TableParams {
pagination?: TablePaginationConfig;
sortField?: SorterResult<any>['field'];
sortOrder?: SorterResult<any>['order'];
filters?: Parameters<GetProp<TableProps, 'onChange'>>[1];
}
const columns: ColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
sorter: true,
width: '20%',
},
{
title: 'Gender',
dataIndex: 'gender',
filters: [
{ text: 'Male', value: 'male' },
{ text: 'Female', value: 'female' },
],
width: '20%',
},
{
title: 'Email',
dataIndex: 'email',
},
];
const isNonNullable = <T,>(val: T): val is NonNullable<T> => {
return val !== undefined && val !== null;
};
const toURLSearchParams = <T extends Record<PropertyKey, any>>(record: T) => {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(record)) {
params.append(key, value);
}
return params;
};
const getRandomuserParams = (params: TableParams) => {
const { pagination, filters, sortField, sortOrder, ...restParams } = params;
const result: Record<string, any> = {};
// https://github.com/mockapi-io/docs/wiki/Code-examples#pagination
result.limit = pagination?.pageSize;
result.page = pagination?.current;
// https://github.com/mockapi-io/docs/wiki/Code-examples#filtering
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (isNonNullable(value)) {
result[key] = value;
}
});
}
// https://github.com/mockapi-io/docs/wiki/Code-examples#sorting
if (sortField) {
result.orderby = sortField;
result.order = sortOrder === 'ascend' ? 'asc' : 'desc';
}
// 处理其他参数
Object.entries(restParams).forEach(([key, value]) => {
if (isNonNullable(value)) {
result[key] = value;
}
});
return result;
};
const App: React.FC = () => {
const [data, setData] = useState<DataType[]>();
const [loading, setLoading] = useState(false);
const [tableParams, setTableParams] = useState<TableParams>({
pagination: {
current: 1,
pageSize: 10,
},
});
const params = toURLSearchParams(getRandomuserParams(tableParams));
const fetchData = () => {
setLoading(true);
fetch(`https://660d2bd96ddfa2943b33731c.mockapi.io/api/users?${params.toString()}`)
.then((res) => res.json())
.then((res) => {
setData(Array.isArray(res) ? res : []);
setLoading(false);
setTableParams({
...tableParams,
pagination: {
...tableParams.pagination,
total: 100,
// 100 is mock data, you should read it from server
// total: data.totalCount,
},
});
})
.catch(() => {
console.log('fetch mock data failed');
});
};
useEffect(fetchData, [
tableParams.pagination?.current,
tableParams.pagination?.pageSize,
tableParams?.sortOrder,
tableParams?.sortField,
JSON.stringify(tableParams.filters),
]);
const handleTableChange: TableProps<DataType>['onChange'] = (pagination, filters, sorter) => {
setTableParams({
pagination,
filters,
sortOrder: Array.isArray(sorter) ? undefined : sorter.order,
sortField: Array.isArray(sorter) ? undefined : sorter.field,
});
// `dataSource` is useless since `pageSize` changed
if (pagination.pageSize !== tableParams.pagination?.pageSize) {
setData([]);
}
};
return (
<Table<DataType>
columns={columns}
rowKey={(record) => record.id}
dataSource={data}
pagination={tableParams.pagination}
loading={loading}
onChange={handleTableChange}
/>
);
};
export default App;
크기 (size)
medium과 small 두 가지 컴팩트 테이블 크기가 있습니다. small 크기는 Modal에서만 사용됩니다.
import React from 'react';
import { Divider, Table } from 'antd';
import type { TableColumnsType } from 'antd';
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Age',
dataIndex: 'age',
},
{
title: 'Address',
dataIndex: 'address',
},
];
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
},
];
const App: React.FC = () => (
<>
<Divider>Medium size table</Divider>
<Table<DataType> columns={columns} dataSource={data} size="medium" />
<Divider>Small size table</Divider>
<Table<DataType> columns={columns} dataSource={data} size="small" />
</>
);
export default App;
테두리, 제목과 푸터 (border, title and footer)
테이블에 테두리, 제목, 푸터를 추가합니다.
import React from 'react';
import { Table } from 'antd';
import type { TableProps } from 'antd';
interface DataType {
key: string;
name: string;
money: string;
address: string;
}
const columns: TableProps<DataType>['columns'] = [
{
title: 'Name',
dataIndex: 'name',
render: (text) => <a>{text}</a>,
},
{
title: 'Cash Assets',
className: 'column-money',
dataIndex: 'money',
align: 'right',
},
{
title: 'Address',
dataIndex: 'address',
},
];
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
money: '¥300,000.00',
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
money: '¥1,256,000.00',
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
money: '¥120,000.00',
address: 'Sydney No. 1 Lake Park',
},
];
const App: React.FC = () => (
<Table<DataType>
columns={columns}
dataSource={data}
bordered
title={() => 'Header'}
footer={() => 'Footer'}
/>
);
export default App;
확장 가능한 행 (Expandable Row)
보여줄 정보가 너무 많아 테이블이 한 번에 모두 표시할 수 없을 때 사용합니다.
import React from 'react';
import { Table } from 'antd';
import type { TableColumnsType } from 'antd';
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
description: string;
}
const columns: TableColumnsType<DataType> = [
{ title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Age', dataIndex: 'age', key: 'age' },
{ title: 'Address', dataIndex: 'address', key: 'address' },
{
title: 'Action',
dataIndex: '',
key: 'x',
render: () => <a>Delete</a>,
},
];
const data: DataType[] = [
{
key: 1,
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
description: 'My name is John Brown, I am 32 years old, living in New York No. 1 Lake Park.',
},
{
key: 2,
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
description: 'My name is Jim Green, I am 42 years old, living in London No. 1 Lake Park.',
},
{
key: 3,
name: 'Not Expandable',
age: 29,
address: 'Jiangsu No. 1 Lake Park',
description: 'This not expandable',
},
{
key: 4,
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
description: 'My name is Joe Black, I am 32 years old, living in Sydney No. 1 Lake Park.',
},
];
const App: React.FC = () => (
<Table<DataType>
columns={columns}
expandable={{
expandedRowRender: (record) => <p style={{ margin: 0 }}>{record.description}</p>,
rowExpandable: (record) => record.name !== 'Not Expandable',
}}
dataSource={data}
/>
);
export default App;
특정 컬럼 순서 지정 (Order Specific Column)
Table.EXPAND_COLUMN과 Table.SELECTION_COLUMN을 사용해 확장 및 선택 컬럼의 순서를 제어할 수 있습니다.
import React from 'react';
import { Table } from 'antd';
import type { TableColumnsType } from 'antd';
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
description: string;
}
const columns: TableColumnsType<DataType> = [
{ title: 'Name', dataIndex: 'name', key: 'name' },
Table.EXPAND_COLUMN,
{ title: 'Age', dataIndex: 'age', key: 'age' },
Table.SELECTION_COLUMN,
{ title: 'Address', dataIndex: 'address', key: 'address' },
];
const data: DataType[] = [
{
key: 1,
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
description: 'My name is John Brown, I am 32 years old, living in New York No. 1 Lake Park.',
},
{
key: 2,
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
description: 'My name is Jim Green, I am 42 years old, living in London No. 1 Lake Park.',
},
{
key: 3,
name: 'Not Expandable',
age: 29,
address: 'Jiangsu No. 1 Lake Park',
description: 'This not expandable',
},
{
key: 4,
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
description: 'My name is Joe Black, I am 32 years old, living in Sydney No. 1 Lake Park.',
},
];
const App: React.FC = () => (
<Table<DataType>
columns={columns}
rowSelection={{}}
expandable={{
expandedRowRender: (record) => <p style={{ margin: 0 }}>{record.description}</p>,
}}
dataSource={data}
/>
);
export default App;
colSpan과 rowSpan
테이블 컬럼 제목은 column에 설정하는 colSpan을 지원합니다.
테이블 셀은 onCell 반환 객체에 설정하는 colSpan과 rowSpan을 지원합니다. 각각 0으로 설정하면 해당 셀은 렌더링되지 않습니다.
import React from 'react';
import { Table } from 'antd';
import type { TableProps } from 'antd';
interface DataType {
key: string;
name: string;
age: number;
tel: string;
phone: number;
address: string;
}
// In the fifth row, other columns are merged into first column
// by setting it's colSpan to be 0
const sharedOnCell = (_: DataType, index?: number) => {
if (index === 1) {
return { colSpan: 0 };
}
return {};
};
const columns: TableProps<DataType>['columns'] = [
{
title: 'RowHead',
dataIndex: 'key',
rowScope: 'row',
},
{
title: 'Name',
dataIndex: 'name',
render: (text) => <a>{text}</a>,
onCell: (_, index) => ({
colSpan: index === 1 ? 5 : 1,
}),
},
{
title: 'Age',
dataIndex: 'age',
onCell: sharedOnCell,
},
{
title: 'Home phone',
colSpan: 2,
dataIndex: 'tel',
onCell: (_, index) => {
if (index === 3) {
return { rowSpan: 2 };
}
// These two are merged into above cell
if (index === 4) {
return { rowSpan: 0 };
}
if (index === 1) {
return { colSpan: 0 };
}
return {};
},
},
{
title: 'Phone',
colSpan: 0,
dataIndex: 'phone',
onCell: sharedOnCell,
},
{
title: 'Address',
dataIndex: 'address',
onCell: sharedOnCell,
},
];
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
age: 32,
tel: '0571-22098909',
phone: 18889898989,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
tel: '0571-22098333',
phone: 18889898888,
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
tel: '0575-22098909',
phone: 18900010002,
address: 'Sydney No. 1 Lake Park',
},
{
key: '4',
name: 'Jim Red',
age: 18,
tel: '0575-22098909',
phone: 18900010002,
address: 'London No. 2 Lake Park',
},
{
key: '5',
name: 'Jake White',
age: 18,
tel: '0575-22098909',
phone: 18900010002,
address: 'Dublin No. 2 Lake Park',
},
];
const App: React.FC = () => <Table<DataType> columns={columns} dataSource={data} bordered />;
export default App;
트리 데이터 (Tree data)
dataSource에 children이라는 key 필드가 있을 때 Table에 트리 구조 데이터를 표시합니다. 트리 테이블 구조를 피하려면 childrenColumnName 속성을 커스터마이즈해 보세요.
indentSize를 설정해 들여쓰기 너비를 제어할 수 있습니다.
import React, { useState } from 'react';
import { Space, Switch, Table } from 'antd';
import type { TableColumnsType, TableProps } from 'antd';
type TableRowSelection<T extends object = object> = TableProps<T>['rowSelection'];
interface DataType {
key: React.ReactNode;
name: string;
age: number;
address: string;
children?: DataType[];
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
width: '12%',
},
{
title: 'Address',
dataIndex: 'address',
width: '30%',
key: 'address',
},
];
const data: DataType[] = [
{
key: 1,
name: 'John Brown sr.',
age: 60,
address: 'New York No. 1 Lake Park',
children: [
{
key: 11,
name: 'John Brown',
age: 42,
address: 'New York No. 2 Lake Park',
},
{
key: 12,
name: 'John Brown jr.',
age: 30,
address: 'New York No. 3 Lake Park',
children: [
{
key: 121,
name: 'Jimmy Brown',
age: 16,
address: 'New York No. 3 Lake Park',
},
],
},
{
key: 13,
name: 'Jim Green sr.',
age: 72,
address: 'London No. 1 Lake Park',
children: [
{
key: 131,
name: 'Jim Green',
age: 42,
address: 'London No. 2 Lake Park',
children: [
{
key: 1311,
name: 'Jim Green jr.',
age: 25,
address: 'London No. 3 Lake Park',
},
{
key: 1312,
name: 'Jimmy Green sr.',
age: 18,
address: 'London No. 4 Lake Park',
},
],
},
],
},
],
},
{
key: 2,
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
},
];
// rowSelection objects indicates the need for row selection
const rowSelection: TableRowSelection<DataType> = {
onChange: (selectedRowKeys, selectedRows, info) => {
console.log(
`selectedRowKeys: ${selectedRowKeys}`,
'selectedRows: ',
selectedRows,
'info',
info,
);
},
onSelect: (record, selected, selectedRows) => {
console.log(record, selected, selectedRows);
},
};
const App: React.FC = () => {
const [checkStrictly, setCheckStrictly] = useState(false);
return (
<>
<Space align="center" style={{ marginBottom: 16 }}>
CheckStrictly: <Switch checked={checkStrictly} onChange={setCheckStrictly} />
</Space>
<Table<DataType>
columns={columns}
rowSelection={{ ...rowSelection, checkStrictly }}
dataSource={data}
/>
</>
);
};
export default App;
고정 헤더 (Fixed Header)
많은 양의 데이터를 스크롤 가능한 뷰에 표시합니다.
헤더와 셀이 제대로 정렬되지 않으면 컬럼의 너비를 지정하세요. 지정한 너비가 동작하지 않거나 컬럼 사이에 여백(gutter)이 있다면, 유동 레이아웃에 맞게 최소한 하나의 컬럼은 너비 없이 남겨두거나, 테이블 레이아웃을 깨뜨리는 긴 단어가 없는지 확인해 보세요.
import React from 'react';
import { Table } from 'antd';
import type { TableColumnsType } from 'antd';
import { createStyles } from 'antd-style';
const useStyle = createStyles(({ css, prefixCls }) => {
const antCls = `.${prefixCls}`;
return {
customTable: css`
${antCls}-table {
${antCls}-table-container {
${antCls}-table-body,
${antCls}-table-content {
scrollbar-width: thin;
scrollbar-color: #eaeaea transparent;
}
}
}
`,
};
});
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
width: 150,
},
{
title: 'Age',
dataIndex: 'age',
width: 150,
},
{
title: 'Address',
dataIndex: 'address',
},
];
const dataSource = Array.from({ length: 100 }).map<DataType>((_, i) => ({
key: i,
name: `Edward King ${i}`,
age: 32,
address: `London, Park Lane no. ${i}`,
}));
const App: React.FC = () => {
const { styles } = useStyle();
return (
<Table<DataType>
className={styles.customTable}
columns={columns}
dataSource={dataSource}
pagination={{ pageSize: 50 }}
scroll={{ y: 55 * 5 }}
/>
);
};
export default App;
자동 높이 (Auto height)
Table을 감싸 컨테이너 높이를 자동으로 항상 채우게 만듭니다.
import React, { useEffect, useRef, useState } from 'react';
import { Flex, Switch, Table } from 'antd';
import type { GetRef, TableColumnsType, TableProps } from 'antd';
// ===================== HOC =====================
const measureClassNames = {
header: 'measure-header',
pagination: 'measure-pagination',
};
const tableClassNames = {
header: {
wrapper: measureClassNames.header,
},
pagination: {
root: measureClassNames.pagination,
},
};
type AutoHeightTableProps<RecordType extends object> = Omit<
TableProps<RecordType>,
'styles' | 'classNames'
>;
const AutoHeightTable = <RecordType extends object>(props: AutoHeightTableProps<RecordType>) => {
const { scroll, style, ...restProps } = props;
const rootRef = useRef<GetRef<typeof Table>>(null);
const [scrollY, setScrollY] = useState(0);
const [sectionHeight, setSectionHeight] = useState(0);
const getHeight = (className: string | HTMLElement) => {
const ele =
typeof className === 'string'
? rootRef.current?.nativeElement?.querySelector<HTMLElement>(`.${className}`)
: className;
if (ele) {
const styles = getComputedStyle(ele);
const marginTop = Number.parseFloat(styles.marginTop) || 0;
const marginBottom = Number.parseFloat(styles.marginBottom) || 0;
return ele.getBoundingClientRect().height + marginTop + marginBottom;
}
return 0;
};
useEffect(() => {
const element = rootRef.current?.nativeElement;
if (!element) {
return;
}
const measure = () => {
const totalHeight = getHeight(element);
const headerHeight = getHeight(measureClassNames.header);
const paginationHeight = getHeight(measureClassNames.pagination);
setScrollY(Math.max(0, Math.floor(totalHeight - headerHeight - paginationHeight)));
setSectionHeight(totalHeight - paginationHeight);
};
measure();
const resizeObserver = new ResizeObserver(measure);
resizeObserver.observe(element);
return () => {
resizeObserver.disconnect();
};
}, []);
return (
<Table<RecordType>
{...restProps}
ref={rootRef}
scroll={{ ...scroll, y: scrollY }}
style={{ ...style, height: '100%' }}
styles={{
section: {
height: sectionHeight,
},
}}
classNames={tableClassNames}
/>
);
};
// ==================== Usage ====================
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
width: '20%',
},
{
title: 'Age',
dataIndex: 'age',
width: '20%',
},
{
title: 'Address',
dataIndex: 'address',
width: '60%',
},
];
const genData = (length: number): DataType[] => {
return Array.from({ length }).map<DataType>((_, index) => ({
key: index,
name: `Edward King ${index}`,
age: 32 + index,
address: `London, Park Lane no. ${index}`,
}));
};
const dataMore = genData(30);
const dataLess = genData(2);
const App: React.FC = () => {
const [hasData, setHasData] = useState(true);
const mergedData = hasData ? dataMore : dataLess;
return (
<Flex vertical gap="middle" align="start">
<Switch
checked={hasData}
checkedChildren="More Data"
unCheckedChildren="More Data"
onChange={setHasData}
/>
<div
style={{
height: 400,
boxSizing: 'border-box',
background: 'rgba(140, 140, 140, 0.03)',
padding: 16,
alignSelf: 'stretch',
}}
>
<AutoHeightTable<DataType> columns={columns} dataSource={mergedData} />
</div>
</Flex>
);
};
export default App;
고정 컬럼 (Fixed Columns)
일부 컬럼을 고정하고 다른 컬럼 안에서 스크롤하려면, 동시에 scroll.x를 설정해야 합니다.
헤더와 셀이 제대로 정렬되지 않으면 컬럼의 너비를 지정하세요. 지정한 너비가 동작하지 않거나 컬럼 사이에 여백(gutter)이 있다면, 유동 레이아웃에 맞게 최소한 하나의 컬럼은 너비 없이 남겨두거나, 테이블 레이아웃을 깨뜨리는 긴 단어가 없는지 확인해 보세요.
scroll.x에는 테이블 너비보다 큰 고정값을 권장합니다. 고정되지 않은 컬럼의 합은scroll.x보다 크지 않아야 합니다.
참고: v4는 고정 효과 구현에 sticky를 사용합니다. IE 11은 가로 스크롤로 다운그레이드됩니다.
import React from 'react';
import { Table } from 'antd';
import type { TableColumnsType } from 'antd';
import { createStyles } from 'antd-style';
const useStyle = createStyles(({ css, prefixCls }) => {
const antCls = `.${prefixCls}`;
return {
customTable: css`
${antCls}-table {
${antCls}-table-container {
${antCls}-table-body,
${antCls}-table-content {
scrollbar-width: thin;
scrollbar-color: #eaeaea transparent;
}
}
}
`,
};
});
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Full Name',
width: 100,
dataIndex: 'name',
key: 'name',
fixed: 'start',
},
{
title: 'Age',
width: 100,
dataIndex: 'age',
key: 'age',
fixed: 'start',
sorter: true,
},
{ title: 'Column 1', dataIndex: 'address', key: '1' },
{ title: 'Column 2', dataIndex: 'address', key: '2' },
{ title: 'Column 3', dataIndex: 'address', key: '3' },
{ title: 'Column 4', dataIndex: 'address', key: '4' },
{ title: 'Column 5', dataIndex: 'address', key: '5' },
{ title: 'Column 6', dataIndex: 'address', key: '6' },
{ title: 'Column 7', dataIndex: 'address', key: '7' },
{ title: 'Column 8', dataIndex: 'address', key: '8' },
{ title: 'Column 9', dataIndex: 'address', key: '9' },
{ title: 'Column 10', dataIndex: 'address', key: '10' },
{ title: 'Column 11', dataIndex: 'address', key: '11' },
{ title: 'Column 12', dataIndex: 'address', key: '12' },
{ title: 'Column 13', dataIndex: 'address', key: '13' },
{ title: 'Column 14', dataIndex: 'address', key: '14' },
{ title: 'Column 15', dataIndex: 'address', key: '15' },
{ title: 'Column 16', dataIndex: 'address', key: '16' },
{ title: 'Column 17', dataIndex: 'address', key: '17' },
{ title: 'Column 18', dataIndex: 'address', key: '18' },
{ title: 'Column 19', dataIndex: 'address', key: '19' },
{ title: 'Column 20', dataIndex: 'address', key: '20' },
{
title: 'Action',
key: 'operation',
fixed: 'end',
width: 100,
render: () => <a>action</a>,
},
];
const dataSource: DataType[] = [
{ key: '1', name: 'Olivia', age: 32, address: 'New York Park' },
{ key: '2', name: 'Ethan', age: 40, address: 'London Park' },
];
const App: React.FC = () => {
const { styles } = useStyle();
return (
<Table<DataType>
className={styles.customTable}
pagination={false}
columns={columns}
dataSource={dataSource}
scroll={{ x: 'max-content' }}
/>
);
};
export default App;
스택 고정 컬럼 (Stack Fixed Columns)
일정 거리를 스크롤했을 때만 컬럼이 고정되고, 계속 스크롤하면 다른 컬럼들이 쌓입니다. bordered와 함께 사용을 권장합니다.
import React from 'react';
import { Table } from 'antd';
import type { TableColumnsType } from 'antd';
import { createStyles } from 'antd-style';
const useStyle = createStyles(({ css, prefixCls }) => {
const antCls = `.${prefixCls}`;
return {
customTable: css`
${antCls}-table {
${antCls}-table-container {
${antCls}-table-body,
${antCls}-table-content {
scrollbar-width: thin;
scrollbar-color: #eaeaea transparent;
}
}
}
`,
};
});
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Full Name',
width: 100,
dataIndex: 'name',
fixed: 'start',
},
{
title: 'Age',
width: 100,
dataIndex: 'age',
},
{ title: 'Column 1', dataIndex: 'address', key: '1', fixed: 'start' },
{ title: 'Column 2', dataIndex: 'address', key: '2' },
{ title: 'Column 3', dataIndex: 'address', key: '3' },
{ title: 'Column 4', dataIndex: 'address', key: '4' },
{ title: 'Column 5', dataIndex: 'address', key: '5' },
{ title: 'Column 6', dataIndex: 'address', key: '6' },
{ title: 'Column 7', dataIndex: 'address', key: '7' },
{ title: 'Column 8', dataIndex: 'address', key: '8' },
{ title: 'Column 9', dataIndex: 'address', key: '9' },
{ title: 'Column 10', dataIndex: 'address', key: '10' },
{ title: 'Column 11', dataIndex: 'address', key: '11' },
{ title: 'Column 12', dataIndex: 'address', key: '12' },
{ title: 'Column 13', dataIndex: 'address', key: '13' },
{ title: 'Column 14', dataIndex: 'address', key: '14' },
{ title: 'Column 15', dataIndex: 'address', key: '15' },
{ title: 'Column 16', dataIndex: 'address', key: '16' },
{ title: 'Column 17', dataIndex: 'address', key: '17' },
{ title: 'Column 18', dataIndex: 'address', key: '18' },
{ title: 'Column 19', dataIndex: 'address', key: '19' },
{ title: 'Column 20', dataIndex: 'address', key: '20' },
{
title: 'Action 1',
fixed: 'end',
width: 90,
render: () => <a>action</a>,
},
{
title: 'Action 2',
width: 90,
render: () => <a>action</a>,
},
{
title: 'Action 3',
fixed: 'end',
width: 90,
render: () => <a>action</a>,
},
];
const dataSource: DataType[] = [
{ key: '1', name: 'Olivia', age: 32, address: 'New York Park' },
{ key: '2', name: 'Ethan', age: 40, address: 'London Park' },
];
const App: React.FC = () => {
const { styles } = useStyle();
return (
<Table<DataType>
bordered
className={styles.customTable}
columns={columns}
dataSource={dataSource}
scroll={{ x: 'max-content' }}
pagination={false}
/>
);
};
export default App;
고정 컬럼과 헤더 (Fixed Columns and Header)
긴 컬럼의 많은 데이터를 표시하기 위한 해결책입니다.
헤더와 셀이 제대로 정렬되지 않으면 컬럼의 너비를 지정하세요. 지정한 너비가 동작하지 않거나 컬럼 사이에 여백(gutter)이 있다면, 유동 레이아웃에 맞게 최소한 하나의 컬럼은 너비 없이 남겨두거나, 테이블 레이아웃을 깨뜨리는 긴 단어가 없는지 확인해 보세요.
scroll.x에는 테이블 너비보다 큰 고정값을 권장합니다. 고정되지 않은 컬럼의 합은scroll.x보다 크지 않아야 합니다.
import React from 'react';
import { Table } from 'antd';
import type { TableColumnsType } from 'antd';
import { createStyles } from 'antd-style';
const useStyle = createStyles(({ css, prefixCls }) => {
const antCls = `.${prefixCls}`;
return {
customTable: css`
${antCls}-table {
${antCls}-table-container {
${antCls}-table-body,
${antCls}-table-content {
scrollbar-width: thin;
scrollbar-color: #eaeaea transparent;
}
}
}
`,
};
});
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Full Name',
width: 100,
dataIndex: 'name',
key: 'name',
fixed: 'start',
},
{
title: 'Age',
width: 100,
dataIndex: 'age',
key: 'age',
fixed: 'start',
},
{
title: 'Column 1',
dataIndex: 'address',
key: '1',
width: 150,
},
{
title: 'Column 2',
dataIndex: 'address',
key: '2',
width: 150,
},
{
title: 'Column 3',
dataIndex: 'address',
key: '3',
width: 150,
},
{
title: 'Column 4',
dataIndex: 'address',
key: '4',
width: 150,
},
{
title: 'Column 5',
dataIndex: 'address',
key: '5',
width: 150,
},
{
title: 'Column 6',
dataIndex: 'address',
key: '6',
width: 150,
},
{
title: 'Column 7',
dataIndex: 'address',
key: '7',
width: 150,
},
{ title: 'Column 8', dataIndex: 'address', key: '8' },
{ title: 'Column 9', dataIndex: 'address', key: '9' },
{ title: 'Column 10', dataIndex: 'address', key: '10' },
{ title: 'Column 11', dataIndex: 'address', key: '11' },
{ title: 'Column 12', dataIndex: 'address', key: '12' },
{ title: 'Column 13', dataIndex: 'address', key: '13' },
{ title: 'Column 14', dataIndex: 'address', key: '14' },
{ title: 'Column 15', dataIndex: 'address', key: '15' },
{ title: 'Column 16', dataIndex: 'address', key: '16' },
{ title: 'Column 17', dataIndex: 'address', key: '17' },
{ title: 'Column 18', dataIndex: 'address', key: '18' },
{ title: 'Column 19', dataIndex: 'address', key: '19' },
{ title: 'Column 20', dataIndex: 'address', key: '20' },
{
title: 'Action',
key: 'operation',
fixed: 'end',
width: 100,
render: () => <a>action</a>,
},
];
const dataSource = Array.from({ length: 100 }).map<DataType>((_, i) => ({
key: i,
name: `Edward King ${i}`,
age: 32,
address: `London, Park Lane no. ${i}`,
}));
const App: React.FC = () => {
const { styles } = useStyle();
return (
<Table<DataType>
className={styles.customTable}
columns={columns}
dataSource={dataSource}
scroll={{ x: 'max-content', y: 55 * 5 }}
/>
);
};
export default App;
숨김 컬럼 (Hidden Columns)
hidden으로 컬럼을 숨깁니다.
import React, { useState } from 'react';
import { Checkbox, Divider, Table } from 'antd';
import type { CheckboxOptionType, TableColumnsType } from 'antd';
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{ title: 'Column 1', dataIndex: 'address', key: '1' },
{ title: 'Column 2', dataIndex: 'address', key: '2' },
{ title: 'Column 3', dataIndex: 'address', key: '3' },
{ title: 'Column 4', dataIndex: 'address', key: '4' },
{ title: 'Column 5', dataIndex: 'address', key: '5' },
{ title: 'Column 6', dataIndex: 'address', key: '6' },
{ title: 'Column 7', dataIndex: 'address', key: '7' },
{ title: 'Column 8', dataIndex: 'address', key: '8' },
];
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York Park',
},
{
key: '2',
name: 'Jim Green',
age: 40,
address: 'London Park',
},
];
const defaultCheckedList = columns.map((item) => item.key);
const App: React.FC = () => {
const [checkedList, setCheckedList] = useState(defaultCheckedList);
const options = columns.map(({ key, title }) => ({
label: title,
value: key,
}));
const newColumns = columns.map((item) => ({
...item,
hidden: !checkedList.includes(item.key as string),
}));
return (
<>
<Divider>Columns displayed</Divider>
<Checkbox.Group
value={checkedList}
options={options as CheckboxOptionType[]}
onChange={(value) => {
setCheckedList(value as string[]);
}}
/>
<Table<DataType> columns={newColumns} dataSource={data} style={{ marginTop: 24 }} />
</>
);
};
export default App;
테이블 헤더 그룹화 (Grouping table head)
columns[n].children으로 테이블 헤더를 그룹화합니다.
import React from 'react';
import { Table } from 'antd';
import type { TableColumnsType } from 'antd';
import { createStyles } from 'antd-style';
const useStyle = createStyles(({ css, prefixCls }) => {
const antCls = `.${prefixCls}`;
return {
customTable: css`
${antCls}-table {
${antCls}-table-container {
${antCls}-table-body,
${antCls}-table-content {
scrollbar-width: thin;
scrollbar-color: #eaeaea transparent;
}
}
}
`,
};
});
interface DataType {
key: React.Key;
name: string;
age: number;
street: string;
building: string;
number: number;
companyAddress: string;
companyName: string;
gender: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 100,
fixed: 'start',
filters: [
{
text: 'Joe',
value: 'Joe',
},
{
text: 'John',
value: 'John',
},
],
onFilter: (value, record) => record.name.indexOf(value as string) === 0,
},
{
title: 'Other',
children: [
{
title: 'Age',
dataIndex: 'age',
key: 'age',
width: 150,
sorter: (a, b) => a.age - b.age,
},
{
title: 'Address',
children: [
{
title: 'Street',
dataIndex: 'street',
key: 'street',
width: 150,
},
{
title: 'Block',
children: [
{
title: 'Building',
dataIndex: 'building',
key: 'building',
width: 100,
},
{
title: 'Door No.',
dataIndex: 'number',
key: 'number',
width: 100,
},
],
},
],
},
],
},
{
title: 'Company',
children: [
{
title: 'Company Address',
dataIndex: 'companyAddress',
key: 'companyAddress',
width: 200,
},
{
title: 'Company Name',
dataIndex: 'companyName',
key: 'companyName',
},
],
},
{
title: 'Gender',
dataIndex: 'gender',
key: 'gender',
width: 80,
fixed: 'end',
},
];
const dataSource = Array.from({ length: 100 }).map<DataType>((_, i) => ({
key: i,
name: 'John Brown',
age: i + 1,
street: 'Lake Park',
building: 'C',
number: 2035,
companyAddress: 'Lake Street 42',
companyName: 'SoftLake Co',
gender: 'M',
}));
const App: React.FC = () => {
const { styles } = useStyle();
return (
<Table<DataType>
className={styles.customTable}
columns={columns}
dataSource={dataSource}
bordered
size="medium"
scroll={{ x: 'calc(700px + 50%)', y: 47 * 5 }}
/>
);
};
export default App;
편집 가능한 셀 (Editable Cells)
편집 가능한 셀이 있는 테이블입니다. shouldCellUpdate와 함께 사용할 때는 클로저에 주의하세요.
import React, { useContext, useEffect, useRef, useState } from 'react';
import type { GetRef, InputRef, TableProps } from 'antd';
import { Button, Form, Input, Popconfirm, Table } from 'antd';
import { createStyles } from 'antd-style';
const useStyles = createStyles((props) => {
const { css, cssVar } = props;
return {
editableRow: css`
position: relative;
.editable-cell-value-wrap {
cursor: pointer;
padding: ${cssVar.paddingXXS} ${cssVar.paddingSM};
border-width: ${cssVar.lineWidth};
border-style: ${cssVar.lineType};
border-color: transparent;
border-radius: ${cssVar.borderRadiusSM};
transition: all ${cssVar.motionDurationFast} ${cssVar.motionEaseInOut};
}
&:hover {
.editable-cell-value-wrap {
border-color: ${cssVar.colorBorder};
}
}
`,
};
});
type FormInstance<T> = GetRef<typeof Form<T>>;
const EditableContext = React.createContext<FormInstance<any> | null>(null);
interface Item {
key: string;
name: string;
age: string;
address: string;
}
interface EditableRowProps {
index: number;
}
const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => {
const [form] = Form.useForm();
return (
<Form form={form} component={false}>
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
</Form>
);
};
interface EditableCellProps {
title: React.ReactNode;
editable: boolean;
dataIndex: keyof Item;
record: Item;
handleSave: (record: Item) => void;
}
const EditableCell: React.FC<React.PropsWithChildren<EditableCellProps>> = (props) => {
const { title, editable, children, dataIndex, record, handleSave, ...restProps } = props;
const [editing, setEditing] = useState(false);
const inputRef = useRef<InputRef>(null);
const form = useContext(EditableContext)!;
useEffect(() => {
if (editing) {
inputRef.current?.focus();
}
}, [editing]);
const toggleEdit = () => {
setEditing((prev) => !prev);
form.setFieldsValue({ [dataIndex]: record[dataIndex] });
};
const save = async () => {
try {
const values = await form.validateFields();
toggleEdit();
handleSave({ ...record, ...values });
} catch (errInfo) {
console.log('Save failed:', errInfo);
}
};
let childNode = children;
if (editable) {
childNode = editing ? (
<Form.Item
style={{ margin: 0 }}
name={dataIndex}
rules={[{ required: true, message: `${title} is required.` }]}
>
<Input ref={inputRef} variant="filled" onPressEnter={save} onBlur={save} />
</Form.Item>
) : (
<div
className="editable-cell-value-wrap"
style={{ paddingInlineEnd: 24 }}
onClick={toggleEdit}
>
{children}
</div>
);
}
return <td {...restProps}>{childNode}</td>;
};
interface DataType {
key: React.Key;
name: string;
age: string;
address: string;
}
type ColumnTypes = Exclude<TableProps<DataType>['columns'], undefined>;
const App: React.FC = () => {
const { styles } = useStyles();
const [dataSource, setDataSource] = useState<DataType[]>([
{
key: '0',
name: 'Edward King 0',
age: '32',
address: 'London, Park Lane no. 0',
},
{
key: '1',
name: 'Edward King 1',
age: '32',
address: 'London, Park Lane no. 1',
},
]);
const [count, setCount] = useState(2);
const handleDelete = (key: React.Key) => {
const newData = dataSource.filter((item) => item.key !== key);
setDataSource(newData);
};
const defaultColumns: (ColumnTypes[number] & { editable?: boolean; dataIndex: string })[] = [
{
title: 'name',
dataIndex: 'name',
width: '30%',
editable: true,
},
{
title: 'age',
dataIndex: 'age',
},
{
title: 'address',
dataIndex: 'address',
},
{
title: 'operation',
dataIndex: 'operation',
render: (_, record) =>
dataSource.length >= 1 ? (
<Popconfirm title="Sure to delete?" onConfirm={() => handleDelete(record.key)}>
<a>Delete</a>
</Popconfirm>
) : null,
},
];
const handleAdd: React.MouseEventHandler<HTMLElement> = () => {
const newData: DataType = {
key: count,
name: `Edward King ${count}`,
age: '32',
address: `London, Park Lane no. ${count}`,
};
setDataSource([...dataSource, newData]);
setCount((prevCount) => prevCount + 1);
};
const handleSave = (row: DataType) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => row.key === item.key);
if (index !== -1) {
const item = newData[index];
newData.splice(index, 1, { ...item, ...row });
setDataSource(newData);
}
};
const components = {
body: { row: EditableRow, cell: EditableCell },
};
const columns = defaultColumns.map((col) => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: (record: DataType) => ({
record,
editable: col.editable,
dataIndex: col.dataIndex,
title: col.title,
handleSave,
}),
};
});
return (
<div>
<Button onClick={handleAdd} type="primary" style={{ marginBottom: 16 }}>
Add a row
</Button>
<Table<DataType>
components={components}
rowClassName={() => styles.editableRow}
bordered
dataSource={dataSource}
columns={columns as ColumnTypes}
/>
</div>
);
};
export default App;
편집 가능한 행 (Editable Rows)
편집 가능한 행이 있는 테이블입니다.
import React, { useState } from 'react';
import type { TableColumnsType } from 'antd';
import { Form, Input, InputNumber, Popconfirm, Table, Typography } from 'antd';
import { createStyles } from 'antd-style';
const useStyles = createStyles((props) => {
const { css, prefixCls, cssVar } = props;
return {
editableRow: css`
position: relative;
.${prefixCls}-form-item-explain {
position: absolute;
top: 100%;
font-size: ${cssVar.fontSizeSM};
}
`,
};
});
interface DataType {
key: string;
name: string;
age: number;
address: string;
}
const originData = Array.from({ length: 100 }).map<DataType>((_, i) => ({
key: i.toString(),
name: `Edward ${i}`,
age: 32,
address: `London Park no. ${i}`,
}));
interface EditableCellProps extends React.HTMLAttributes<HTMLElement> {
editing: boolean;
dataIndex: string;
title: any;
inputType: 'number' | 'text';
record: DataType;
index: number;
}
const EditableCell: React.FC<React.PropsWithChildren<EditableCellProps>> = (props) => {
const { editing, dataIndex, title, inputType, record, index, children, ...restProps } = props;
const inputNode = inputType === 'number' ? <InputNumber /> : <Input />;
return (
<td {...restProps}>
{editing ? (
<Form.Item
name={dataIndex}
style={{ margin: 0 }}
rules={[{ required: true, message: `Please Input ${title}!` }]}
>
{inputNode}
</Form.Item>
) : (
children
)}
</td>
);
};
const App: React.FC = () => {
const { styles } = useStyles();
const [form] = Form.useForm();
const [data, setData] = useState<DataType[]>(originData);
const [editingKey, setEditingKey] = useState('');
const isEditing = (record: DataType) => record.key === editingKey;
const edit = (record: Partial<DataType> & { key: React.Key }) => {
form.setFieldsValue({ name: '', age: '', address: '', ...record });
setEditingKey(record.key);
};
const cancel = () => {
setEditingKey('');
};
const save = async (key: React.Key) => {
try {
const row = (await form.validateFields()) as DataType;
const newData = [...data];
const index = newData.findIndex((item) => key === item.key);
if (index > -1) {
const item = newData[index];
newData.splice(index, 1, { ...item, ...row });
setData(newData);
setEditingKey('');
} else {
newData.push(row);
setData(newData);
setEditingKey('');
}
} catch (errInfo) {
console.log('Validate Failed:', errInfo);
}
};
const columns = [
{
title: 'name',
dataIndex: 'name',
width: '25%',
editable: true,
},
{
title: 'age',
dataIndex: 'age',
width: '15%',
editable: true,
},
{
title: 'address',
dataIndex: 'address',
width: '40%',
editable: true,
},
{
title: 'operation',
dataIndex: 'operation',
render: (_: any, record: DataType) => {
const editable = isEditing(record);
return editable ? (
<span>
<Typography.Link onClick={() => save(record.key)} style={{ marginInlineEnd: 8 }}>
Save
</Typography.Link>
<Popconfirm title="Sure to cancel?" onConfirm={cancel}>
<a>Cancel</a>
</Popconfirm>
</span>
) : (
<Typography.Link disabled={editingKey !== ''} onClick={() => edit(record)}>
Edit
</Typography.Link>
);
},
},
];
const mergedColumns = columns.map<TableColumnsType<DataType>[number]>((col) => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: (record) => ({
record,
inputType: col.dataIndex === 'age' ? 'number' : 'text',
dataIndex: col.dataIndex,
title: col.title,
editing: isEditing(record),
}),
};
});
return (
<Form form={form} component={false}>
<Table<DataType>
bordered
components={{ body: { cell: EditableCell } }}
dataSource={data}
columns={mergedColumns}
rowClassName={() => styles.editableRow}
pagination={{ onChange: cancel }}
/>
</Form>
);
};
export default App;
중첩 테이블 (Nested tables)
각 행의 더 상세한 정보를 보여줍니다.
import React from 'react';
import { DownOutlined } from '@ant-design/icons';
import type { TableColumnsType } from 'antd';
import { Badge, Dropdown, Space, Table } from 'antd';
interface ExpandedDataType {
key: React.Key;
date: string;
name: string;
upgradeNum: string;
}
interface DataType {
key: React.Key;
name: string;
platform: string;
version: string;
upgradeNum: number;
creator: string;
createdAt: string;
}
const items = [
{ key: '1', label: 'Action 1' },
{ key: '2', label: 'Action 2' },
];
const expandDataSource = Array.from({ length: 3 }).map<ExpandedDataType>((_, i) => ({
key: i.toString(),
date: '2014-12-24 23:12:00',
name: 'This is production name',
upgradeNum: 'Upgraded: 56',
}));
const dataSource = Array.from({ length: 3 }).map<DataType>((_, i) => ({
key: i.toString(),
name: 'Screen',
platform: 'iOS',
version: '10.3.4.5654',
upgradeNum: 500,
creator: 'Jack',
createdAt: '2014-12-24 23:12:00',
}));
const expandColumns: TableColumnsType<ExpandedDataType> = [
{ title: 'Date', dataIndex: 'date', key: 'date' },
{ title: 'Name', dataIndex: 'name', key: 'name' },
{
title: 'Status',
key: 'state',
render: () => <Badge status="success" text="Finished" />,
},
{ title: 'Upgrade Status', dataIndex: 'upgradeNum', key: 'upgradeNum' },
{
title: 'Action',
key: 'operation',
render: () => (
<Space size="medium">
<a>Pause</a>
<a>Stop</a>
<Dropdown menu={{ items }}>
<a>
More <DownOutlined />
</a>
</Dropdown>
</Space>
),
},
];
const columns: TableColumnsType<DataType> = [
{ title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Platform', dataIndex: 'platform', key: 'platform' },
{ title: 'Version', dataIndex: 'version', key: 'version' },
{ title: 'Upgraded', dataIndex: 'upgradeNum', key: 'upgradeNum' },
{ title: 'Creator', dataIndex: 'creator', key: 'creator' },
{ title: 'Date', dataIndex: 'createdAt', key: 'createdAt' },
{ title: 'Action', key: 'operation', render: () => <a>Publish</a> },
];
const expandedRowRender = () => (
<Table<ExpandedDataType>
columns={expandColumns}
dataSource={expandDataSource}
pagination={false}
/>
);
const App: React.FC = () => (
<>
<Table<DataType>
columns={columns}
expandable={{ expandedRowRender, defaultExpandedRowKeys: ['0'] }}
dataSource={dataSource}
/>
<Table<DataType>
columns={columns}
expandable={{ expandedRowRender, defaultExpandedRowKeys: ['0'] }}
dataSource={dataSource}
size="medium"
/>
<Table<DataType>
columns={columns}
expandable={{ expandedRowRender, defaultExpandedRowKeys: ['0'] }}
dataSource={dataSource}
size="small"
/>
</>
);
export default App;
드래그 정렬 (Drag sorting)
components를 사용해 테이블을 dnd-kit과 통합해 드래그 정렬 기능을 구현할 수 있습니다.
import React, { useState } from 'react';
import type { DragEndEvent } from '@dnd-kit/core';
import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Table } from 'antd';
import type { TableColumnsType } from 'antd';
interface DataType {
key: string;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Age',
dataIndex: 'age',
},
{
title: 'Address',
dataIndex: 'address',
},
];
interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
'data-row-key': string;
}
const Row: React.FC<Readonly<RowProps>> = (props) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: props['data-row-key'],
});
const style: React.CSSProperties = {
...props.style,
transform: CSS.Translate.toString(transform),
transition,
cursor: 'move',
...(isDragging ? { position: 'relative', zIndex: 9999 } : {}),
};
return <tr {...props} ref={setNodeRef} style={style} {...attributes} {...listeners} />;
};
const App: React.FC = () => {
const [dataSource, setDataSource] = useState([
{
key: '1',
name: 'John Brown',
age: 32,
address:
'Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text Long text',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
},
]);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
// https://docs.dndkit.com/api-documentation/sensors/pointer#activation-constraints
distance: 1,
},
}),
);
const onDragEnd = ({ active, over }: DragEndEvent) => {
if (active.id !== over?.id) {
setDataSource((prev) => {
const activeIndex = prev.findIndex((i) => i.key === active.id);
const overIndex = prev.findIndex((i) => i.key === over?.id);
return arrayMove(prev, activeIndex, overIndex);
});
}
};
return (
<DndContext sensors={sensors} modifiers={[restrictToVerticalAxis]} onDragEnd={onDragEnd}>
<SortableContext
// rowKey array
items={dataSource.map((i) => i.key)}
strategy={verticalListSortingStrategy}
>
<Table<DataType>
components={{
body: { row: Row },
}}
rowKey="key"
columns={columns}
dataSource={dataSource}
/>
</SortableContext>
</DndContext>
);
};
export default App;
드래그 컬럼 정렬 (Drag Column sorting)
components를 사용해 테이블을 dnd-kit과 통합해 컬럼 드래그 정렬 기능을 구현할 수 있습니다.
import React, { createContext, useContext, useState } from 'react';
import type { DragEndEvent, DragOverEvent, UniqueIdentifier } from '@dnd-kit/core';
import {
closestCenter,
DndContext,
DragOverlay,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { restrictToHorizontalAxis } from '@dnd-kit/modifiers';
import {
arrayMove,
horizontalListSortingStrategy,
SortableContext,
useSortable,
} from '@dnd-kit/sortable';
import { Table } from 'antd';
import type { TableColumnsType } from 'antd';
interface DataType {
key: string;
name: string;
gender: string;
age: number;
email: string;
address: string;
}
interface HeaderCellProps extends React.HTMLAttributes<HTMLTableCellElement> {
id: string;
}
interface BodyCellProps extends React.HTMLAttributes<HTMLTableCellElement> {
id: string;
}
interface DragIndexState {
active: UniqueIdentifier;
over: UniqueIdentifier | undefined;
direction?: 'left' | 'right';
}
const DragIndexContext = createContext<DragIndexState>({ active: -1, over: -1 });
const dragActiveStyle = (dragState: DragIndexState, id: string) => {
const { active, over, direction } = dragState;
// drag active style
let style: React.CSSProperties = {};
if (active && active === id) {
style = { backgroundColor: 'gray', opacity: 0.5 };
} else if (over && id === over && active !== over) {
style =
direction === 'right'
? { borderInlineEnd: '1px dashed gray' }
: { borderInlineStart: '1px dashed gray' };
}
return style;
};
const TableBodyCell: React.FC<BodyCellProps> = (props) => {
const dragState = useContext<DragIndexState>(DragIndexContext);
return <td {...props} style={{ ...props.style, ...dragActiveStyle(dragState, props.id) }} />;
};
const TableHeaderCell: React.FC<HeaderCellProps> = (props) => {
const dragState = useContext(DragIndexContext);
const { attributes, listeners, setNodeRef, isDragging } = useSortable({ id: props.id });
const style: React.CSSProperties = {
...props.style,
cursor: 'move',
...(isDragging ? { position: 'relative', zIndex: 9999, userSelect: 'none' } : {}),
...dragActiveStyle(dragState, props.id),
};
return <th {...props} ref={setNodeRef} style={style} {...attributes} {...listeners} />;
};
const dataSource: DataType[] = [
{
key: '1',
name: 'John Brown',
gender: 'male',
age: 32,
email: 'John [email protected]',
address: 'London No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
gender: 'female',
age: 42,
email: '[email protected]',
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
gender: 'female',
age: 32,
email: '[email protected]',
address: 'Sidney No. 1 Lake Park',
},
{
key: '4',
name: 'George Hcc',
gender: 'male',
age: 20,
email: '[email protected]',
address: 'Sidney No. 1 Lake Park',
},
];
const baseColumns: TableColumnsType<DataType> = [
{ title: 'Name', dataIndex: 'name' },
{ title: 'Gender', dataIndex: 'gender' },
{ title: 'Age', dataIndex: 'age' },
{ title: 'Email', dataIndex: 'email' },
{ title: 'Address', dataIndex: 'address' },
];
const App: React.FC = () => {
const [dragIndex, setDragIndex] = useState<DragIndexState>({ active: -1, over: -1 });
const [columns, setColumns] = useState(() =>
baseColumns.map((column, i) => ({
...column,
key: `${i}`,
onHeaderCell: () => ({ id: `${i}` }),
onCell: () => ({ id: `${i}` }),
})),
);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
// https://docs.dndkit.com/api-documentation/sensors/pointer#activation-constraints
distance: 1,
},
}),
);
const onDragEnd = ({ active, over }: DragEndEvent) => {
if (active.id !== over?.id) {
setColumns((prevState) => {
const activeIndex = prevState.findIndex((i) => i.key === active?.id);
const overIndex = prevState.findIndex((i) => i.key === over?.id);
return arrayMove(prevState, activeIndex, overIndex);
});
}
setDragIndex({ active: -1, over: -1 });
};
const onDragOver = ({ active, over }: DragOverEvent) => {
const activeIndex = columns.findIndex((i) => i.key === active.id);
const overIndex = columns.findIndex((i) => i.key === over?.id);
setDragIndex({
active: active.id,
over: over?.id,
direction: overIndex > activeIndex ? 'right' : 'left',
});
};
return (
<DndContext
sensors={sensors}
modifiers={[restrictToHorizontalAxis]}
onDragEnd={onDragEnd}
onDragOver={onDragOver}
collisionDetection={closestCenter}
>
<SortableContext items={columns.map((i) => i.key)} strategy={horizontalListSortingStrategy}>
<DragIndexContext.Provider value={dragIndex}>
<Table<DataType>
rowKey="key"
columns={columns}
dataSource={dataSource}
components={{
header: { cell: TableHeaderCell },
body: { cell: TableBodyCell },
}}
/>
</DragIndexContext.Provider>
</SortableContext>
<DragOverlay>
<th style={{ backgroundColor: 'gray', padding: 16 }}>
{columns[columns.findIndex((i) => i.key === dragIndex.active)]?.title as React.ReactNode}
</th>
</DragOverlay>
</DndContext>
);
};
export default App;
핸들러로 드래그 정렬 (Drag sorting with handler)
또는 dnd-kit으로 핸들러를 사용해 드래그 정렬을 구현할 수 있습니다.
import React, { useContext, useMemo } from 'react';
import { HolderOutlined } from '@ant-design/icons';
import type { DragEndEvent } from '@dnd-kit/core';
import { DndContext } from '@dnd-kit/core';
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, Table } from 'antd';
import type { TableColumnsType } from 'antd';
interface DataType {
key: string;
name: string;
age: number;
address: string;
}
interface RowContextProps {
setActivatorNodeRef?: (element: HTMLElement | null) => void;
listeners?: SyntheticListenerMap;
}
const RowContext = React.createContext<RowContextProps>({});
const DragHandle: React.FC = () => {
const { setActivatorNodeRef, listeners } = useContext(RowContext);
return (
<Button
type="text"
size="small"
icon={<HolderOutlined />}
style={{ cursor: 'move' }}
ref={setActivatorNodeRef}
{...listeners}
/>
);
};
const columns: TableColumnsType<DataType> = [
{ key: 'sort', align: 'center', width: 80, render: () => <DragHandle /> },
{ title: 'Name', dataIndex: 'name' },
{ title: 'Age', dataIndex: 'age' },
{ title: 'Address', dataIndex: 'address' },
];
const initialData: DataType[] = [
{ key: '1', name: 'John Brown', age: 32, address: 'Long text Long' },
{ key: '2', name: 'Jim Green', age: 42, address: 'London No. 1 Lake Park' },
{ key: '3', name: 'Joe Black', age: 32, address: 'Sidney No. 1 Lake Park' },
];
interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
'data-row-key': string;
}
const Row: React.FC<RowProps> = (props) => {
const {
attributes,
listeners,
setNodeRef,
setActivatorNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: props['data-row-key'] });
const style: React.CSSProperties = {
...props.style,
transform: CSS.Translate.toString(transform),
transition,
...(isDragging ? { position: 'relative', zIndex: 9999 } : {}),
};
const contextValue = useMemo<RowContextProps>(
() => ({ setActivatorNodeRef, listeners }),
[setActivatorNodeRef, listeners],
);
return (
<RowContext.Provider value={contextValue}>
<tr {...props} ref={setNodeRef} style={style} {...attributes} />
</RowContext.Provider>
);
};
const App: React.FC = () => {
const [dataSource, setDataSource] = React.useState<DataType[]>(initialData);
const onDragEnd = ({ active, over }: DragEndEvent) => {
if (active.id !== over?.id) {
setDataSource((prevState) => {
const activeIndex = prevState.findIndex((record) => record.key === active?.id);
const overIndex = prevState.findIndex((record) => record.key === over?.id);
return arrayMove(prevState, activeIndex, overIndex);
});
}
};
return (
<DndContext modifiers={[restrictToVerticalAxis]} onDragEnd={onDragEnd}>
<SortableContext items={dataSource.map((i) => i.key)} strategy={verticalListSortingStrategy}>
<Table<DataType>
rowKey="key"
components={{ body: { row: Row } }}
columns={columns}
dataSource={dataSource}
/>
</SortableContext>
</DndContext>
);
};
export default App;
말줄임 컬럼 (ellipsis column)
column.ellipsis를 설정해 셀 콘텐츠를 말줄임 처리합니다.
현재는 정렬과 필터가 있는 테이블 헤더는 말줄임 처리할 수 없습니다.
import React from 'react';
import { Table } from 'antd';
import type { TableColumnsType } from 'antd';
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (text) => <a>{text}</a>,
width: 150,
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
width: 80,
},
{
title: 'Address',
dataIndex: 'address',
key: 'address 1',
ellipsis: true,
},
{
title: 'Long Column Long Column Long Column',
dataIndex: 'address',
key: 'address 2',
ellipsis: true,
},
{
title: 'Long Column Long Column',
dataIndex: 'address',
key: 'address 3',
ellipsis: true,
},
{
title: 'Long Column',
dataIndex: 'address',
key: 'address 4',
ellipsis: true,
},
];
const data = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park, New York No. 1 Lake Park',
tags: ['nice', 'developer'],
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 2 Lake Park, London No. 2 Lake Park',
tags: ['kawaii'],
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park, Sydney No. 1 Lake Park',
tags: ['cool', 'teacher'],
},
];
const App: React.FC = () => <Table<DataType> columns={columns} dataSource={data} />;
export default App;
공유 컬럼 props (Shared column props)
column을 사용해 테이블에 공유 컬럼 props를 설정하고, 필요할 때 컬럼마다 덮어씁니다.
import React from 'react';
import type { TableProps } from 'antd';
import { Table } from 'antd';
interface DataType {
key: React.Key;
name: string;
address: string;
description: string;
}
const columns: TableProps<DataType>['columns'] = [
{
title: 'Name',
dataIndex: 'name',
width: 140,
},
{
title: 'Description',
dataIndex: 'description',
width: 180,
},
{
title: 'Address',
dataIndex: 'address',
align: 'left',
width: 220,
},
];
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
description: 'Shared column props let repeated column settings live on the table.',
address: 'No. 1 Lake Park Road, Hangzhou, Zhejiang, China',
},
{
key: '2',
name: 'Jim Green',
description: 'Columns can still override the default alignment or other shared props.',
address: 'No. 99 Garden Avenue, Pudong, Shanghai, China',
},
];
const App: React.FC = () => (
<Table<DataType>
columns={columns}
dataSource={data}
column={{ align: 'center', ellipsis: true }}
pagination={false}
/>
);
export default App;
말줄임 컬럼 커스텀 툴팁 (ellipsis column custom tooltip)
column.ellipsis.showTitle을 설정해 셀 콘텐츠를 말줄임 처리하고, html title 속성 대신 Tooltip을 사용합니다.
import React from 'react';
import { Table, Tooltip } from 'antd';
import type { TableColumnsType } from 'antd';
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (text) => <a>{text}</a>,
width: 150,
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
width: 80,
},
{
title: 'Address',
dataIndex: 'address',
key: 'address 1',
ellipsis: {
showTitle: false,
},
render: (address) => (
<Tooltip placement="topLeft" title={address}>
{address}
</Tooltip>
),
},
{
title: 'Long Column Long Column Long Column',
dataIndex: 'address',
key: 'address 2',
ellipsis: {
showTitle: false,
},
render: (address) => (
<Tooltip placement="topLeft" title={address}>
{address}
</Tooltip>
),
},
{
title: 'Long Column Long Column',
dataIndex: 'address',
key: 'address 3',
ellipsis: {
showTitle: false,
},
render: (address) => (
<Tooltip placement="topLeft" title={address}>
{address}
</Tooltip>
),
},
{
title: 'Long Column',
dataIndex: 'address',
key: 'address 4',
ellipsis: {
showTitle: false,
},
render: (address) => (
<Tooltip placement="topLeft" title={address}>
{address}
</Tooltip>
),
},
];
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park, New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 2 Lake Park, London No. 2 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park, Sydney No. 1 Lake Park',
},
];
const App: React.FC = () => <Table<DataType> columns={columns} dataSource={data} />;
export default App;
커스텀 빈 상태 (Custom empty)
커스텀 빈 상태를 표시합니다.
import React, { useState } from 'react';
import type { GetProp } from 'antd';
import { Button, ConfigProvider, Empty, Table } from 'antd';
interface DataType {
key: number;
name: string;
age: number;
address: string;
}
const genFakeData = (count = 5) =>
Array.from({ length: count }).map<DataType>((_, index) => ({
key: index,
name: `Edward King ${index}`,
age: 32 + index,
address: `London, Park Lane no. ${index}`,
}));
const renderEmpty: GetProp<typeof ConfigProvider, 'renderEmpty'> = (componentName) => {
if (componentName === 'Table.filter' /** 👈 5.20.0+ */) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="No Filter(Custom)" />;
}
};
function App() {
const [dataSource, setDataSource] = useState<DataType[]>(genFakeData);
const handleToggle = () => {
setDataSource(dataSource.length ? [] : genFakeData(Math.floor(Math.random() * 10)));
};
const columns: GetProp<typeof Table<DataType>, 'columns'> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
filters: dataSource.length
? [
{ text: '>=35', value: 'gte35' },
{ text: '<18', value: 'lt18' },
]
: [],
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
},
];
const toggleButton = (
<Button type="primary" onClick={handleToggle}>
Toggle Data
</Button>
);
return (
<ConfigProvider renderEmpty={renderEmpty}>
{dataSource.length ? toggleButton : null}
<div style={{ marginBlock: 8 }} />
<Table<DataType>
bordered
dataSource={dataSource}
columns={columns}
locale={{ emptyText: <Empty description="No Data">{toggleButton}</Empty> }}
/>
</ConfigProvider>
);
}
export default App;
요약 (Summary)
summary prop으로 요약 콘텐츠를 설정합니다. Table.Summary.Cell로 컬럼 고정 상태를 동기화합니다. Table.Summary의 fixed prop으로 고정할 수 있습니다(4.16.0부터).
import React from 'react';
import { Flex, Table, Typography } from 'antd';
import type { TableColumnsType } from 'antd';
import { createStyles } from 'antd-style';
const useStyle = createStyles(({ css, prefixCls }) => {
const antCls = `.${prefixCls}`;
return {
customTable: css`
${antCls}-table {
${antCls}-table-container {
${antCls}-table-body,
${antCls}-table-content {
scrollbar-width: thin;
scrollbar-color: #eaeaea transparent;
}
}
}
`,
};
});
const { Text } = Typography;
interface DataType {
key: string;
name: string;
borrow: number;
repayment: number;
}
interface FixedDataType {
key: React.Key;
name: string;
description: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Borrow',
dataIndex: 'borrow',
},
{
title: 'Repayment',
dataIndex: 'repayment',
},
];
const dataSource: DataType[] = [
{
key: '1',
name: 'John Brown',
borrow: 10,
repayment: 33,
},
{
key: '2',
name: 'Jim Green',
borrow: 100,
repayment: 0,
},
{
key: '3',
name: 'Joe Black',
borrow: 10,
repayment: 10,
},
{
key: '4',
name: 'Jim Red',
borrow: 75,
repayment: 45,
},
];
const fixedColumns: TableColumnsType<FixedDataType> = [
{
title: 'Name',
dataIndex: 'name',
fixed: true,
width: 100,
},
{
title: 'Description',
dataIndex: 'description',
},
];
const fixedDataSource = Array.from({ length: 20 }).map<FixedDataType>((_, i) => ({
key: i,
name: ['Light', 'Bamboo', 'Little'][i % 3],
description: 'Everything that has a beginning, has an end.',
}));
const App: React.FC = () => {
const { styles } = useStyle();
return (
<Flex vertical gap="small">
<Table<DataType>
bordered
className={styles.customTable}
columns={columns}
dataSource={dataSource}
pagination={false}
summary={(pageData) => {
let totalBorrow = 0;
let totalRepayment = 0;
pageData.forEach(({ borrow, repayment }) => {
totalBorrow += borrow;
totalRepayment += repayment;
});
return (
<>
<Table.Summary.Row>
<Table.Summary.Cell index={0}>Total</Table.Summary.Cell>
<Table.Summary.Cell index={1}>
<Text type="danger">{totalBorrow}</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={2}>
<Text>{totalRepayment}</Text>
</Table.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell index={0}>Balance</Table.Summary.Cell>
<Table.Summary.Cell index={1} colSpan={2}>
<Text type="danger">{totalBorrow - totalRepayment}</Text>
</Table.Summary.Cell>
</Table.Summary.Row>
</>
);
}}
/>
<Table<FixedDataType>
className={styles.customTable}
columns={fixedColumns}
dataSource={fixedDataSource}
pagination={false}
scroll={{ x: 2000, y: 500 }}
bordered
summary={() => (
<Table.Summary fixed>
<Table.Summary.Row>
<Table.Summary.Cell index={0}>Summary</Table.Summary.Cell>
<Table.Summary.Cell index={1}>This is a summary content</Table.Summary.Cell>
</Table.Summary.Row>
</Table.Summary>
)}
/>
</Flex>
);
};
export default App;
가상 리스트 (Virtual list)
virtual을 설정해 가상 스크롤을 활성화하고, scroll.x와 scroll.y를 number 타입으로 동시에 설정해야 합니다.
import React from 'react';
import { Button, Segmented, Space, Switch, Table, Typography } from 'antd';
import type { TableProps } from 'antd';
interface RecordType {
id: number;
firstName: string;
lastName: string;
age: number;
address1: string;
address2: string;
address3: string;
}
const fixedColumns: TableProps<RecordType>['columns'] = [
{
title: 'ID',
dataIndex: 'id',
width: 100,
fixed: 'start',
},
{
title: 'FistName',
dataIndex: 'firstName',
width: 120,
fixed: 'start',
},
{
title: 'LastName',
dataIndex: 'lastName',
width: 120,
fixed: 'start',
},
{
title: 'Group',
width: 120,
render: (_, record) => `Group ${Math.floor(record.id / 4)}`,
onCell: (record) => ({
rowSpan: record.id % 4 === 0 ? 4 : 0,
}),
},
{
title: 'Age',
dataIndex: 'age',
width: 100,
onCell: (record) => ({
colSpan: record.id % 4 === 0 ? 2 : 1,
}),
},
{
title: 'Address 1',
dataIndex: 'address1',
onCell: (record) => ({
colSpan: record.id % 4 === 0 ? 0 : 1,
}),
},
{
title: 'Address 2',
dataIndex: 'address2',
},
{
title: 'Address 3',
dataIndex: 'address3',
},
{
title: 'Action',
width: 150,
fixed: 'end',
render: () => (
<Space>
<Typography.Link>Action1</Typography.Link>
<Typography.Link>Action2</Typography.Link>
</Space>
),
},
];
const columns: TableProps<RecordType>['columns'] = [
{
title: 'ID',
dataIndex: 'id',
width: 100,
},
{
title: 'FistName',
dataIndex: 'firstName',
width: 120,
},
{
title: 'LastName',
dataIndex: 'lastName',
},
];
const getData = (length: number) =>
Array.from({ length }).map<RecordType>((_, index) => ({
id: index,
firstName: `First_${index.toString(16)}`,
lastName: `Last_${index.toString(16)}`,
age: 25 + (index % 10),
address1: `New York No. ${index} Lake Park`,
address2: `London No. ${index} Lake Park`,
address3: `Sydney No. ${index} Lake Park`,
}));
const App: React.FC = () => {
const [fixed, setFixed] = React.useState(true);
const [bordered, setBordered] = React.useState(true);
const [expanded, setExpanded] = React.useState(false);
const [empty, setEmpty] = React.useState(false);
const [count, setCount] = React.useState(10000);
const tblRef: Parameters<typeof Table>[0]['ref'] = React.useRef(null);
const data = React.useMemo<RecordType[]>(() => getData(count), [count]);
const mergedColumns = React.useMemo<typeof fixedColumns>(() => {
if (!fixed) {
return columns;
}
if (!expanded) {
return fixedColumns;
}
return fixedColumns.map((col) => ({ ...col, onCell: undefined }));
}, [expanded, fixed]);
const expandableProps = React.useMemo<TableProps<RecordType>['expandable']>(() => {
if (!expanded) {
return undefined;
}
return {
columnWidth: 48,
expandedRowRender: (record) => <p style={{ margin: 0 }}>🎉 Expanded {record.address1}</p>,
rowExpandable: (record) => record.id % 2 === 0,
};
}, [expanded]);
return (
<div style={{ padding: 64 }}>
<Space vertical style={{ width: '100%' }}>
<Space>
<Switch
checked={bordered}
onChange={() => setBordered(!bordered)}
checkedChildren="Bordered"
unCheckedChildren="Bordered"
/>
<Switch
checked={fixed}
onChange={() => setFixed(!fixed)}
checkedChildren="Fixed"
unCheckedChildren="Fixed"
/>
<Switch
checked={expanded}
onChange={() => setExpanded(!expanded)}
checkedChildren="Expandable"
unCheckedChildren="Expandable"
/>
<Switch
checked={empty}
onChange={() => setEmpty(!empty)}
checkedChildren="Empty"
unCheckedChildren="Empty"
/>
<Segmented
value={count}
onChange={setCount}
options={[
{ label: 'None', value: 0 },
{ label: 'Less', value: 4 },
{ label: 'Lot', value: 10000 },
]}
/>
{data.length >= 999 && (
<Button onClick={() => tblRef.current?.scrollTo({ index: 999 })}>
Scroll To index 999
</Button>
)}
</Space>
<Table<RecordType>
bordered={bordered}
virtual
columns={mergedColumns}
scroll={{ x: 2000, y: 400 }}
rowKey="id"
dataSource={empty ? [] : data}
pagination={false}
ref={tblRef}
rowSelection={expanded ? undefined : { type: 'radio', columnWidth: 48 }}
expandable={expandableProps}
/>
</Space>
</div>
);
};
export default App;
반응형 (Responsive)
반응형 컬럼입니다.
import React from 'react';
import { Table } from 'antd';
import type { TableColumnsType } from 'antd';
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Name (all screens)',
dataIndex: 'name',
key: 'name',
render: (text) => <a>{text}</a>,
},
{
title: 'Age (medium screen or bigger)',
dataIndex: 'age',
key: 'age',
responsive: ['md'],
},
{
title: 'Address (large screen or bigger)',
dataIndex: 'address',
key: 'address',
responsive: ['lg'],
},
];
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
];
const App: React.FC = () => <Table<DataType> columns={columns} dataSource={data} />;
export default App;
페이징 설정 (Pagination Settings)
테이블 페이징 설정입니다.
import React, { useState } from 'react';
import { Flex, Radio, Space, Table, Tag } from 'antd';
import type { TableProps } from 'antd';
type ColumnsType<T extends object> = TableProps<T>['columns'];
type TablePagination<T extends object> = NonNullable<Exclude<TableProps<T>['pagination'], boolean>>;
type TablePaginationPlacement<T extends object> = NonNullable<
TablePagination<T>['placement']
>[number];
interface DataType {
key: string;
name: string;
age: number;
address: string;
tags: string[];
}
const topOptions = [
{ label: 'topStart', value: 'topStart' },
{ label: 'topCenter', value: 'topCenter' },
{ label: 'topEnd', value: 'topEnd' },
{ label: 'none', value: 'none' },
];
const bottomOptions = [
{ label: 'bottomStart', value: 'bottomStart' },
{ label: 'bottomCenter', value: 'bottomCenter' },
{ label: 'bottomEnd', value: 'bottomEnd' },
{ label: 'none', value: 'none' },
];
const columns: ColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (text) => <a>{text}</a>,
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
},
{
title: 'Tags',
key: 'tags',
dataIndex: 'tags',
render: (tags: string[]) => (
<Flex gap="small" align="center" wrap>
{tags.map((tag) => {
let color = tag.length > 5 ? 'geekblue' : 'green';
if (tag === 'kawaii') {
color = 'volcano';
}
return (
<Tag color={color} key={tag}>
{tag.toUpperCase()}
</Tag>
);
})}
</Flex>
),
},
{
title: 'Action',
key: 'action',
render: (_, record) => (
<Space size="medium">
<a>Invite {record.name}</a>
<a>Delete</a>
</Space>
),
},
];
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer'],
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['kawaii'],
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
tags: ['cool', 'teacher'],
},
];
const App: React.FC = () => {
const [top, setTop] = useState<TablePaginationPlacement<DataType>>('topStart');
const [bottom, setBottom] = useState<TablePaginationPlacement<DataType>>('bottomEnd');
return (
<div>
<div>
<Radio.Group
style={{ marginBottom: 10 }}
options={topOptions}
value={top}
onChange={(e) => {
setTop(e.target.value);
}}
/>
</div>
<Radio.Group
style={{ marginBottom: 10 }}
options={bottomOptions}
value={bottom}
onChange={(e) => {
setBottom(e.target.value);
}}
/>
<Table<DataType>
columns={columns}
pagination={{ placement: [top, bottom] }}
dataSource={data}
/>
</div>
);
};
export default App;
페이지와 함께 고정되는 헤더와 스크롤바 (Fixed header and scroll bar with the page)
긴 테이블은 헤더와 스크롤바를 보기 위해 스크롤해야 합니다. 이제 고정 헤더와 스크롤바가 페이지를 따라오도록 설정할 수 있습니다.
import React, { useState } from 'react';
import { Switch, Table } from 'antd';
import type { TableColumnsType } from 'antd';
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const columns: TableColumnsType<DataType> = [
{
title: 'Full Name',
width: 100,
dataIndex: 'name',
key: 'name',
fixed: 'start',
},
{
title: 'Age',
width: 100,
dataIndex: 'age',
key: 'age',
fixed: 'start',
},
{
title: 'Column 1',
dataIndex: 'address',
key: '1',
width: 150,
},
{
title: 'Column 2',
dataIndex: 'address',
key: '2',
width: 150,
},
{
title: 'Column 3',
dataIndex: 'address',
key: '3',
width: 150,
},
{
title: 'Column 4',
dataIndex: 'address',
key: '4',
width: 150,
},
{
title: 'Column 5',
dataIndex: 'address',
key: '5',
width: 150,
},
{
title: 'Column 6',
dataIndex: 'address',
key: '6',
width: 150,
},
{
title: 'Column 7',
dataIndex: 'address',
key: '7',
width: 150,
},
{ title: 'Column 8', dataIndex: 'address', key: '8' },
{
title: 'Action',
key: 'operation',
fixed: 'end',
width: 100,
render: () => <a>action</a>,
},
];
const dataSource = Array.from({ length: 100 }).map<DataType>((_, i) => ({
key: i,
name: `Edward ${i}`,
age: 32,
address: `London Park no. ${i}`,
}));
const App: React.FC = () => {
const [fixedTop, setFixedTop] = useState(false);
return (
<Table<DataType>
columns={columns}
dataSource={dataSource}
scroll={{ x: 1500 }}
summary={() => (
<Table.Summary fixed={fixedTop ? 'top' : 'bottom'}>
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={2}>
<Switch
checkedChildren="Fixed Top"
unCheckedChildren="Fixed Top"
checked={fixedTop}
onChange={() => {
setFixedTop(!fixedTop);
}}
/>
</Table.Summary.Cell>
<Table.Summary.Cell index={2} colSpan={8}>
Scroll Context
</Table.Summary.Cell>
<Table.Summary.Cell index={10}>Fix Right</Table.Summary.Cell>
</Table.Summary.Row>
</Table.Summary>
)}
// antd site header height
sticky={{ offsetHeader: 64 }}
/>
);
};
export default App;
동적 설정 (Dynamic Settings)
다른 설정을 선택해 결과를 확인합니다.
import React, { useState } from 'react';
import { DownOutlined } from '@ant-design/icons';
import type { GetProp, RadioChangeEvent, TableProps } from 'antd';
import { Form, Radio, Space, Switch, Table } from 'antd';
import { createStyles } from 'antd-style';
const useStyles = createStyles((props) => {
const { css, cssVar } = props;
return {
tableControlBar: css`
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
margin-bottom: ${cssVar.margin};
row-gap: ${cssVar.marginXS};
column-gap: 0;
`,
};
});
type SizeType = TableProps['size'];
type ColumnsType<T extends object> = GetProp<TableProps<T>, 'columns'>;
type TablePagination<T extends object> = NonNullable<Exclude<TableProps<T>['pagination'], boolean>>;
type TablePaginationPlacement = NonNullable<TablePagination<any>['placement']>[number];
type ExpandableConfig<T extends object> = TableProps<T>['expandable'];
type TableRowSelection<T extends object> = TableProps<T>['rowSelection'];
interface DataType {
key: number;
name: string;
age: number;
address: string;
description: string;
}
const columns: ColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Age',
dataIndex: 'age',
sorter: (a, b) => a.age - b.age,
},
{
title: 'Address',
dataIndex: 'address',
filters: [
{
text: 'London',
value: 'London',
},
{
text: 'New York',
value: 'New York',
},
],
onFilter: (value, record) => record.address.indexOf(value as string) === 0,
},
{
title: 'Action',
key: 'action',
sorter: true,
render: () => (
<Space size="medium">
<a>Delete</a>
<a>
<Space>
More actions
<DownOutlined />
</Space>
</a>
</Space>
),
},
];
const data = Array.from({ length: 10 }).map<DataType>((_, i) => ({
key: i,
name: 'John Brown',
age: Number(`${i}2`),
address: `New York No. ${i} Lake Park`,
description: `My name is John Brown, I am ${i}2 years old, living in New York No. ${i} Lake Park.`,
}));
const defaultExpandable: ExpandableConfig<DataType> = {
expandedRowRender: (record: DataType) => <p>{record.description}</p>,
};
const defaultTitle = () => 'Here is title';
const defaultFooter = () => 'Here is footer';
const App: React.FC = () => {
const { styles } = useStyles();
const [bordered, setBordered] = useState(false);
const [loading, setLoading] = useState(false);
const [size, setSize] = useState<SizeType>('large');
const [expandable, setExpandable] = useState<ExpandableConfig<DataType>>(defaultExpandable);
const [showTitle, setShowTitle] = useState(false);
const [showHeader, setShowHeader] = useState(true);
const [showFooter, setShowFooter] = useState(true);
const [rowSelection, setRowSelection] = useState<TableRowSelection<DataType> | undefined>({});
const [hasData, setHasData] = useState(true);
const [tableLayout, setTableLayout] = useState<string>('unset');
const [top, setTop] = useState<TablePaginationPlacement>('none');
const [bottom, setBottom] = useState<TablePaginationPlacement>('bottomEnd');
const [ellipsis, setEllipsis] = useState(false);
const [yScroll, setYScroll] = useState(false);
const [xScroll, setXScroll] = useState<string>('unset');
const handleBorderChange = (enable: boolean) => {
setBordered(enable);
};
const handleLoadingChange = (enable: boolean) => {
setLoading(enable);
};
const handleSizeChange = (e: RadioChangeEvent) => {
setSize(e.target.value);
};
const handleTableLayoutChange = (e: RadioChangeEvent) => {
setTableLayout(e.target.value);
};
const handleExpandChange = (enable: boolean) => {
setExpandable(enable ? defaultExpandable : undefined);
};
const handleEllipsisChange = (enable: boolean) => {
setEllipsis(enable);
};
const handleTitleChange = (enable: boolean) => {
setShowTitle(enable);
};
const handleHeaderChange = (enable: boolean) => {
setShowHeader(enable);
};
const handleFooterChange = (enable: boolean) => {
setShowFooter(enable);
};
const handleRowSelectionChange = (enable: boolean) => {
setRowSelection(enable ? {} : undefined);
};
const handleYScrollChange = (enable: boolean) => {
setYScroll(enable);
};
const handleXScrollChange = (e: RadioChangeEvent) => {
setXScroll(e.target.value);
};
const handleDataChange = (newHasData: boolean) => {
setHasData(newHasData);
};
const scroll: { x?: number | string; y?: number | string } = {};
if (yScroll) {
scroll.y = 240;
}
if (xScroll !== 'unset') {
scroll.x = '120vw';
}
const tableColumns = columns.map((item) => ({ ...item, ellipsis }));
if (xScroll === 'fixed') {
tableColumns[0].fixed = true;
tableColumns[tableColumns.length - 1].fixed = 'right';
}
const tableProps: TableProps<DataType> = {
bordered,
loading,
size,
expandable,
title: showTitle ? defaultTitle : undefined,
showHeader,
footer: showFooter ? defaultFooter : undefined,
rowSelection,
scroll,
tableLayout: tableLayout === 'unset' ? undefined : (tableLayout as TableProps['tableLayout']),
};
return (
<>
<Form layout="inline" className={styles.tableControlBar}>
<Form.Item label="Bordered">
<Switch checked={bordered} onChange={handleBorderChange} />
</Form.Item>
<Form.Item label="loading">
<Switch checked={loading} onChange={handleLoadingChange} />
</Form.Item>
<Form.Item label="Title">
<Switch checked={showTitle} onChange={handleTitleChange} />
</Form.Item>
<Form.Item label="Column Header">
<Switch checked={showHeader} onChange={handleHeaderChange} />
</Form.Item>
<Form.Item label="Footer">
<Switch checked={showFooter} onChange={handleFooterChange} />
</Form.Item>
<Form.Item label="Expandable">
<Switch checked={!!expandable} onChange={handleExpandChange} />
</Form.Item>
<Form.Item label="Checkbox">
<Switch checked={!!rowSelection} onChange={handleRowSelectionChange} />
</Form.Item>
<Form.Item label="Fixed Header">
<Switch checked={!!yScroll} onChange={handleYScrollChange} />
</Form.Item>
<Form.Item label="Has Data">
<Switch checked={!!hasData} onChange={handleDataChange} />
</Form.Item>
<Form.Item label="Ellipsis">
<Switch checked={!!ellipsis} onChange={handleEllipsisChange} />
</Form.Item>
<Form.Item label="Size">
<Radio.Group value={size} onChange={handleSizeChange}>
<Radio.Button value="large">Large</Radio.Button>
<Radio.Button value="medium">Medium</Radio.Button>
<Radio.Button value="small">Small</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="Table Scroll">
<Radio.Group value={xScroll} onChange={handleXScrollChange}>
<Radio.Button value="unset">Unset</Radio.Button>
<Radio.Button value="scroll">Scroll</Radio.Button>
<Radio.Button value="fixed">Fixed Columns</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="Table Layout">
<Radio.Group value={tableLayout} onChange={handleTableLayoutChange}>
<Radio.Button value="unset">Unset</Radio.Button>
<Radio.Button value="fixed">Fixed</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="Pagination Top">
<Radio.Group value={top} onChange={(e) => setTop(e.target.value)}>
<Radio.Button value="topStart">TopStart</Radio.Button>
<Radio.Button value="topCenter">TopCenter</Radio.Button>
<Radio.Button value="topEnd">TopEnd</Radio.Button>
<Radio.Button value="none">None</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="Pagination Bottom">
<Radio.Group value={bottom} onChange={(e) => setBottom(e.target.value)}>
<Radio.Button value="bottomStart">BottomStart</Radio.Button>
<Radio.Button value="bottomCenter">BottomCenter</Radio.Button>
<Radio.Button value="bottomEnd">BottomEnd</Radio.Button>
<Radio.Button value="none">None</Radio.Button>
</Radio.Group>
</Form.Item>
</Form>
<Table<DataType>
{...tableProps}
pagination={{ placement: [top, bottom] }}
columns={tableColumns}
dataSource={hasData ? data : []}
scroll={scroll}
/>
</>
);
};
export default App;
커스텀 시맨틱 DOM 스타일링 (Custom semantic dom styling)
classNames와 styles에 객체 또는 함수를 넘겨서 Table의 시맨틱 DOM 스타일을 커스터마이즈할 수 있습니다.
import React from 'react';
import { Flex, Table } from 'antd';
import type { GetProp, TableProps } from 'antd';
import { createStaticStyles } from 'antd-style';
const classNames = createStaticStyles(({ css }) => ({
root: css`
color: #e0e0e0;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
`,
}));
interface DataType {
key?: string;
name?: string;
age?: number;
address?: string;
description?: string;
}
const columns: TableProps<DataType>['columns'] = [
{ title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Age', dataIndex: 'age', key: 'age' },
{ title: 'Address', dataIndex: 'address', key: 'address' },
{ title: 'Description', dataIndex: 'description', key: 'description' },
];
const dataSource: DataType[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
description: 'My name is John Brown, I am 32 years old, living in New York No. 1 Lake Park.',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
description: 'My name is Jim Green, I am 42 years old, living in London No. 1 Lake Park.',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
description: 'My name is Joe Black, I am 32 years old, living in Sydney No. 1 Lake Park.',
},
{
key: '4',
name: 'Disabled User',
age: 99,
address: 'Sydney No. 2 Lake Park',
description: 'This user is disabled.',
},
];
const styles: TableProps<DataType>['styles'] = {
root: {
padding: 10,
borderRadius: 8,
},
pagination: {
root: {
padding: 10,
},
},
body: {
row: {
outline: '1px dashed rgba(226, 225, 225, 0.1)',
},
cell: {
outline: '1px dashed rgba(226, 225, 225, 0.1)',
},
},
};
const stylesFn: TableProps<DataType>['styles'] = (
info,
): GetProp<TableProps<DataType>, 'styles', 'Return'> => {
if (info?.props?.size === 'medium') {
return {
root: {
color: '#e0e0e0',
borderRadius: 8,
boxShadow: '0 4px 20px rgba(0,0,0,0.3)',
},
title: {
backgroundImage: 'linear-gradient(90deg, #6a5acd, #836fff)',
color: '#fff',
fontSize: '1.25rem',
fontWeight: 600,
padding: '12px 16px',
},
footer: {
color: '#9ca3af',
},
header: {
cell: {
fontWeight: 600,
fontSize: '0.95rem',
color: '#b8bdfd',
padding: '12px 16px',
borderBottom: '1px solid rgba(255,255,255,0.08)',
},
},
pagination: {
root: {
padding: 10,
},
item: {
color: '#b8bdfd',
},
},
};
}
return {};
};
const App: React.FC = () => {
const sharedProps: TableProps<DataType> = {
columns,
dataSource,
classNames,
pagination: { pageSize: 3, simple: true },
};
return (
<Flex vertical gap="medium">
<Table<DataType>
{...sharedProps}
styles={styles}
title={() => 'Table Object Styles'}
footer={() => 'Table Object Footer'}
size="small"
virtual
scroll={{ y: 300 }}
/>
<Table<DataType>
{...sharedProps}
styles={stylesFn}
title={() => 'Table Function Styles'}
footer={() => 'Table Function Styles'}
size="medium"
/>
</Flex>
);
};
export default App;
API
Common props ref:Common props
Table
| Property | Description | Type | Default | Version | Global Config |
|---|---|---|---|---|---|
| bordered | Whether to show all table borders | boolean | false | × | |
| classNames | Customize class for each semantic structure inside the component. Supports object or function. | Record<SemanticDOM, string> | (info: { props })=> Record<SemanticDOM, string> | - | 6.0.1 | |
| column | Shared props applied to columns when the same property is not defined on the column itself | Partial<ColumnType> | - | 6.4.0 | × |
| columns | Columns of table | ColumnsType[] | - | × | |
| components | Override default table elements | TableComponents | - | × | |
| dataSource | Data record array to be displayed | object[] | - | × | |
| expandable | Config expandable content | expandable | - | expandable.expandIcon: 5.14.0 |
|
| footer | Table footer renderer | function(currentPageData) | - | × | |
| getPopupContainer | The render container of dropdowns in table | (triggerNode) => HTMLElement | () => TableHtmlElement | × | |
| loading | Loading status of table | boolean | Spin Props | false | × | |
| locale | The i18n text including filter, sort, empty text, etc | object | Default Value | × | |
| pagination | Config of pagination. You can ref table pagination config or full pagination document, hide it by setting it to false |
object | false |
- | × | |
| rowClassName | Row's className | function(record, index): string | - | × | |
| rowKey | Row's unique key, could be a string or function that returns a string | string | function(record): string | key |
string: 6.0.0, function: 6.1.0 |
|
| rowSelection | Row selection config | object | - | × | |
| rowHoverable | Row hover | boolean | true | 5.16.0 | × |
| scroll | Whether the table can be scrollable, config | object | - | 6.3.0 | |
| showHeader | Whether to show table header | boolean | true | × | |
| showSorterTooltip | The header show next sorter direction tooltip. It will be set as the property of Tooltip if its type is object | boolean | Tooltip props & {target?: 'full-header' | 'sorter-icon' } |
{ target: 'full-header' } | 5.16.0 | × |
| size | Size of table | large | medium | small |
large |
× | |
| sortDirections | Supported sort way, could be ascend, descend |
Array | [ascend, descend] |
× | |
| sticky | Set sticky header and scroll bar | boolean | {offsetHeader?: number, offsetScroll?: number, getContainer?: () => HTMLElement} |
- | 4.6.0 (getContainer: 4.7.0) | × |
| styles | Customize inline style for each semantic structure inside the component. Supports object or function. | Record<SemanticDOM, CSSProperties> | (info: { props })=> Record<SemanticDOM, CSSProperties> | - | 6.0.1 | |
| summary | Summary content | (currentData) => ReactNode | - | × | |
| tableLayout | The table-layout attribute of table element | - | auto | fixed |
-fixed when header/columns are fixed, or using column.ellipsis |
× | |
| title | Table title renderer | function(currentPageData) | - | × | |
| virtual | Support virtual list | boolean | - | 5.9.0 | × |
| onChange | Callback executed when pagination, filters or sorter is changed | function(pagination, filters, sorter, extra: { currentDataSource: [], action: paginate | sort | filter }) |
- | × | |
| onHeaderRow | Set props on per header row | function(columns, index) | - | × | |
| onRow | Set props on per row | function(record, index) | - | × | |
| onScroll | Triggered when the table body is scrolled. Note that only vertical scrolling will trigger the event when virtual |
function(event) | - | 5.16.0 | × |
Table ref
| Property | Description | Type | Version |
|---|---|---|---|
| nativeElement | The wrap element | HTMLDivElement | 5.11.0 |
| scrollTo | Trigger to scroll to target position. key match with record rowKey. When offset is specified, the table will scroll to align the target row to the top with the given offset and not working with top. Optional align param to control alignment: start align to top, center align to center, end align to bottom, nearest smart align (default). center align is not supported in virtual scrolling mode |
(config: { index?: number, key?: React.Key, top?: number, offset?: number, align?: 'start' | 'center' | 'end' | 'nearest' }) => void | 5.11.0 |
onRow 사용법
onRow와 동일합니다 onHeaderRow onCell onHeaderCell
<Table
onRow={(record, rowIndex) => {
return {
onClick: (event) => {}, // click row
onDoubleClick: (event) => {}, // double click row
onContextMenu: (event) => {}, // right button click row
onMouseEnter: (event) => {}, // mouse enter row
onMouseLeave: (event) => {}, // mouse leave row
};
}}
onHeaderRow={(columns, index) => {
return {
onClick: () => {}, // click header row
};
}}
/>
Column
테이블의 컬럼을 설명하는 Table columns prop 중 하나입니다. Column은 동일한 API를 가집니다.
| Property | Description | Type | Default | Version |
|---|---|---|---|---|
| align | The specify which way that column is aligned | left | right | center |
left |
|
| className | The className of this column | string | - | |
| colSpan | Span of this column's title | number | - | |
| dataIndex | Display field of the data record, support nest path by string array | string | string[] | - | |
| defaultFilteredValue | Default filtered values | string[] | - | |
| filterResetToDefaultFilteredValue | click the reset button, whether to restore the default filter | boolean | false | |
| defaultSortOrder | Default order of sorted values | ascend | descend |
- | |
| ellipsis | The ellipsis cell content, not working with sorter and filters for now. tableLayout would be fixed when ellipsis is true or { showTitle?: boolean } |
boolean | {showTitle?: boolean } | false | showTitle: 4.3.0 |
| filterDropdown | Customized filter overlay | ReactNode | (props: FilterDropdownProps) => ReactNode | - | |
| filtered | Whether the dataSource is filtered |
boolean | false | |
| filteredValue | Controlled filtered value, filter icon will highlight | string[] | - | |
| filterIcon | Customized filter icon | ReactNode | (filtered: boolean) => ReactNode | - | |
| filterOnClose | Whether to trigger filter when the filter menu closes | boolean | true | 5.15.0 |
| filterMultiple | Whether multiple filters can be selected | boolean | true | |
| filterMode | To specify the filter interface | 'menu' | 'tree' | 'menu' | 4.17.0 |
| filterSearch | Whether to be searchable for filter menu | boolean | function(input, record):boolean | false | boolean:4.17.0 function:4.19.0 |
| filters | Filter menu config | object[] | - | |
| filterDropdownProps | Customized dropdown props, filterDropdownOpen and onFilterDropdownOpenChange were available before <5.22.0 |
DropdownProps | - | 5.22.0 |
| fixed | (IE not support) Set column to be fixed: true(same as 'start') 'start' 'end' |
boolean | string | false | |
| key | Unique key of this column, you can ignore this prop if you've set a unique dataIndex |
string | - | |
| render | Renderer of the table cell. value is the value of current cell; record is the value object of current row; index is the row number. The return value should be a ReactNode |
(value: V, record: T, index: number): ReactNode | - | |
| responsive | The list of breakpoints at which to display this column. Always visible if not set | Breakpoint[] | - | 4.2.0 |
| rowScope | Set scope attribute for all cells in this column | row | rowgroup |
- | 5.1.0 |
| shouldCellUpdate | Control cell render logic | (record, prevRecord) => boolean | - | 4.3.0 |
| showSorterTooltip | If header show next sorter direction tooltip, override showSorterTooltip in table |
boolean | Tooltip props & {target?: 'full-header' | 'sorter-icon' } |
{ target: 'full-header' } | 5.16.0 |
| sortDirections | Supported sort way, override sortDirections in Table, could be ascend, descend |
Array | [ascend, descend] |
|
| sorter | Sort function for local sort, see Array.sort's compareFunction. If it is server-side sorting, set to true, but if you want to support multi-column sorting, you can set it to { multiple: number } |
function | boolean | { compare: function, multiple: number } | - | |
| sortOrder | Order of sorted values: ascend descend null |
ascend | descend | null |
- | |
| sortIcon | Customized sort icon | (props: { sortOrder }) => ReactNode | - | 5.6.0 |
| title | Title of this column | ReactNode | ({ sortColumns, filters }) => ReactNode | - | |
| width | Width of this column (width not working?) | string | number | - | |
| minWidth | Min width of this column, only works when tableLayout="auto" |
number | - | 5.21.0 |
| hidden | Hidden this column | boolean | false | 5.13.0 |
| onCell | Set props on per cell | function(record, rowIndex) | - | |
| onFilter | Function that determines if the row is displayed when filtered | function(value, record) => boolean | - | |
| onHeaderCell | Set props on per header cell | function(column) | - |
ColumnGroup
| Property | Description | Type | Default |
|---|---|---|---|
| title | Title of the column group | ReactNode | - |
pagination
페이징을 위한 속성입니다.
| Property | Description | Type | Default |
|---|---|---|---|
| placement | Specify the placement of Pagination, could betopStart | topCenter | topEnd |bottomStart | bottomCenter | bottomEnd | none |
Array | [bottomEnd] |
Specify the position of Pagination, could betopLeft | topCenter | topRight |bottomLeft | bottomCenter | bottomRight | none, please use placement instead |
Array | [bottomRight] |
페이징에 대한 자세한 내용은 Pagination을 확인하세요.
expandable
확장 가능한 행을 위한 속성입니다.
| Property | Description | Type | Default | Version |
|---|---|---|---|---|
| childrenColumnName | The column contains children to display | string | children | |
| columnTitle | Set the title of the expand column | ReactNode | - | 4.23.0 |
| columnWidth | Set the width of the expand column | string | number | - | |
| defaultExpandAllRows | Expand all rows initially | boolean | false | |
| defaultExpandedRowKeys | Initial expanded row keys | string[] | - | |
| expandedRowClassName | Expanded row's className | string | (record, index, indent) => string | - | string: 5.22.0 |
| expandedRowKeys | Current expanded row keys | string[] | - | |
| expandedRowRender | Expanded container render for each row | function(record, index, indent, expanded): ReactNode | - | |
| expandIcon | Customize row expand Icon. Ref example | function(props): ReactNode | - | |
| expandRowByClick | Whether to expand row by clicking anywhere in the whole row | boolean | false | |
| fixed | Whether the expansion icon is fixed. Optional true left right |
boolean | string | false | 4.16.0 |
| forceRender | Force render expanded row content before expansion. In virtual mode, only rows currently mounted by the virtual list are force-rendered; off-screen rows may still be unmounted | boolean | false | 6.6.0 |
| indentSize | Indent size in pixels of tree data | number | 15 | |
| rowExpandable | Enable row can be expandable | (record) => boolean | - | |
| showExpandColumn | Show expand column | boolean | true | 4.18.0 |
| onExpand | Callback executed when the row expand icon is clicked | function(expanded, record) | - | |
| onExpandedRowsChange | Callback executed when the expanded rows change | function(expandedRows) | - | |
| Deprecated: Expand the number of offset columns of the row. After setting, it will force the columns in front of it to be fixed columns. Please use'Table. EXPAND_COLUMN 'instead and control the position through column order | number | - | 5.26.0 |
rowSelection
행 선택을 위한 속성입니다.
| Property | Description | Type | Default | Version |
|---|---|---|---|---|
| align | Set the alignment of selection column | left | right | center |
left |
5.25.0 |
| checkStrictly | Check table row precisely; parent row and children rows are not associated | boolean | true | 4.4.0 |
| columnTitle | Set the title of the selection column | ReactNode | (originalNode: ReactNode) => ReactNode | - | |
| columnWidth | Set the width of the selection column | string | number | 32px |
|
| fixed | Fixed selection column on the left | boolean | - | |
| getCheckboxProps | Get Checkbox or Radio props | function(record) | - | |
| getTitleCheckboxProps | Get title Checkbox props | function() | - | |
| hideSelectAll | Hide the selectAll checkbox and custom selection | boolean | false | 4.3.0 |
| preserveSelectedRowKeys | Keep selection key even when it removed from dataSource |
boolean | - | 4.4.0 |
| renderCell | Renderer of the table cell. Same as render in column |
(checked: boolean, record: T, index: number, originNode: ReactNode): ReactNode | - | 4.1.0 |
| selectedRowKeys | Controlled selected row keys | string[] | number[] | [] | |
| defaultSelectedRowKeys | Default selected row keys | string[] | number[] | [] | |
| selections | Custom selection config, only displays default selections when set to true |
object[] | boolean | - | |
| type | checkbox or radio |
checkbox | radio |
checkbox |
|
| onCell | Set props on per cell. Same as onCell in column |
function(record, rowIndex) | - | 5.5.0 |
| onChange | Callback executed when selected rows change | function(selectedRowKeys, selectedRows, info: { type }) | - | info.type: 4.21.0 |
| onSelect | Callback executed when select/deselect one row | function(record, selected, selectedRows, nativeEvent) | - |
scroll
| Property | Description | Type | Default |
|---|---|---|---|
| scrollToFirstRowOnChange | Whether to scroll to the top of the table when paging, sorting, filtering changes | boolean | - |
| x | Set horizontal scrolling, can also be used to specify the width of the scroll area, could be number, percent value, true and 'max-content' | string | number | true | - |
| y | Set vertical scrolling, can also be used to specify the height of the scroll area, could be string or number | string | number | - |
selection
| Property | Description | Type | Default |
|---|---|---|---|
| key | Unique key of this selection | string | - |
| text | Display text of this selection | ReactNode | - |
| onSelect | Callback executed when this selection is clicked | function(changeableRowKeys) | - |
TypeScript에서 사용하기 (Using in TypeScript) {#using-in-typescript}
import React from 'react';
import { Table } from 'antd';
import type { TableColumnsType } from 'antd';
interface User {
key: number;
name: string;
}
const columns: TableColumnsType<User> = [
{
key: 'name',
title: 'Name',
dataIndex: 'name',
},
];
const data: User[] = [
{
key: 0,
name: 'Jack',
},
];
const Demo: React.FC = () => (
<>
<Table<User> columns={columns} dataSource={data} />
{/* JSX style usage */}
<Table<User> dataSource={data}>
<Table.Column<User> key="name" title="Name" dataIndex="name" />
</Table>
</>
);
export default Demo;
여기에 TypeScript용 CodeSandbox가 있습니다.
시맨틱 DOM (Semantic DOM)
https://ant.design/components/table/semantic.md
디자인 토큰 (Design Token)
컴포넌트 토큰 (Component Token - Table)
| Token Name | Description | Type | Default Value |
|---|---|---|---|
| bodySortBg | Background color of table sorted column | string | #fafafa |
| borderColor | Border color of table | string | #f0f0f0 |
| cellFontSize | Font size of table cell (large size by default) | number | 14 |
| cellFontSizeMD | Font size of table cell (middle size) | number | 14 |
| cellFontSizeSM | Font size of table cell (small size) | number | 14 |
| cellPaddingBlock | Vertical padding of table cell | number | 16 |
| cellPaddingBlockMD | Vertical padding of table cell (middle size) | number | 12 |
| cellPaddingBlockSM | Vertical padding of table cell (small size) | number | 8 |
| cellPaddingInline | Horizontal padding of table cell (large size by default) | number | 16 |
| cellPaddingInlineMD | Horizontal padding of table cell (middle size) | number | 8 |
| cellPaddingInlineSM | Horizontal padding of table cell (small size) | number | 8 |
| expandIconBg | Background of expand button | string | #ffffff |
| filterDropdownBg | Color of filter dropdown | string | #ffffff |
| filterDropdownMenuBg | Background of filter dropdown menu item | string | #ffffff |
| fixedHeaderSortActiveBg | Background color of fixed table header when sorted | string | #f0f0f0 |
| footerBg | Background of footer | string | #fafafa |
| footerColor | Color of footer text | string | rgba(0,0,0,0.88) |
| headerBg | Background of table header | string | #fafafa |
| headerBorderRadius | Border radius of table header | number | 8 |
| headerColor | Color of table header text | string | rgba(0,0,0,0.88) |
| headerFilterHoverBg | Background color of table header filter button when hovered | string | rgba(0,0,0,0.06) |
| headerSortActiveBg | Background color of table header when sorted | string | #f0f0f0 |
| headerSortHoverBg | Background color of table header when sorted and hovered | string | #f0f0f0 |
| headerSplitColor | Split border color of table header | string | #f0f0f0 |
| rowExpandedBg | Background color of table expanded row | string | rgba(0,0,0,0.02) |
| rowHoverBg | Background color of table hovered row | string | #fafafa |
| rowSelectedBg | Background color of table selected row | string | #e6f4ff |
| rowSelectedHoverBg | Background color of table selected row when hovered | string | #bae0ff |
| selectionColumnWidth | Width of selection column | string | number | 32 |
| stickyScrollBarBg | Background of sticky scrollbar | string | rgba(0,0,0,0.25) |
| stickyScrollBarBorderRadius | Border radius of sticky scrollbar | number | 100 |
글로벌 토큰 (Global Token)
| Token Name | Description | Type | Default Value |
|---|---|---|---|
| borderRadius | Border radius of base components | number | |
| boxShadowSecondary | Control the secondary box shadow style of an element. | string | |
| colorBgContainer | Container background color, e.g: default button, input box, etc. Be sure not to confuse this with colorBgElevated. |
string | |
| colorFillQuaternary | The weakest level of fill color is suitable for color blocks that are not easy to attract attention, such as zebra stripes, color blocks that distinguish boundaries, etc. | string | |
| colorIcon | Weak action. Such as allowClear or Alert close button |
string | |
| colorLink | Control the color of hyperlink. | string | |
| colorLinkActive | Control the color of hyperlink when clicked. | string | |
| colorLinkHover | Control the color of hyperlink when hovering. | string | |
| colorPrimary | Brand color is one of the most direct visual elements to reflect the characteristics and communication of the product. After you have selected the brand color, we will automatically generate a complete color palette and assign it effective design semantics. | string | |
| colorPrimaryBorder | The stroke color under the main color gradient, used on the stroke of components such as Slider. | string | |
| colorSplit | Used as the color of separator, this color is the same as colorBorderSecondary but with transparency. | string | |
| colorText | Default text color which comply with W3C standards, and this color is also the darkest neutral color. | string | |
| colorTextDisabled | Control the color of text in disabled state. | string | |
| colorTextHeading | Control the font color of heading. | string | |
| controlInteractiveSize | Control the interactive size of control component. | number | |
| controlItemBgActive | Control the background color of control component item when active. | string | |
| controlItemBgHover | Control the background color of control component item when hovering. | string | |
| fontFamily | The font family of Ant Design prioritizes the default interface font of the system, and provides a set of alternative font libraries that are suitable for screen display to maintain the readability and readability of the font under different platforms and browsers, reflecting the friendly, stable and professional characteristics. | string | |
| fontSize | The most widely used font size in the design system, from which the text gradient will be derived. | number | |
| fontSizeIcon | Control the font size of operation icon in Select, Cascader, etc. Normally same as fontSizeSM. | number | |
| fontSizeSM | Small font size | number | |
| fontWeightStrong | Control the font weight of heading components (such as h1, h2, h3) or selected item. | number | |
| lineHeight | Line height of text. | number | |
| lineType | Border style of base components | string | |
| lineWidth | Border width of base components | number | |
| lineWidthFocus | Control the width of the line when the component is in focus state. | number | |
| linkDecoration | Control the text decoration style of a link. | TextDecoration<string | number> | undefined | |
| linkFocusDecoration | Control the text decoration style of a link on focus. | TextDecoration<string | number> | undefined | |
| linkHoverDecoration | Control the text decoration style of a link on mouse hover. | TextDecoration<string | number> | undefined | |
| margin | Control the margin of an element, with a medium size. | number | |
| marginXXS | Control the margin of an element, with the smallest size. | number | |
| motionDurationMid | Motion speed, medium speed. Used for medium element animation interaction. | string | |
| motionDurationSlow | Motion speed, slow speed. Used for large element animation interaction. | string | |
| opacityLoading | Control the opacity of the loading state. | number | |
| padding | Control the padding of the element. | number | |
| paddingXS | Control the extra small padding of the element. | number | |
| paddingXXS | Control the extra extra small padding of the element. | number |
참고 (Note)
React 문서에 따라 배열의 모든 자식은 고유한 key를 가져야 합니다. Table의 dataSource와 columns 안의 값도 이 규칙을 따라야 합니다. 기본적으로 dataSource[i].key가 dataSource의 key 값으로 처리됩니다.

dataSource[i].key가 제공되지 않았다면 아래처럼 rowKey로 dataSource 값의 기본 key를 지정해야 합니다. 그렇지 않으면 위와 같은 경고가 브라우저 콘솔에 표시됩니다.
// primary key is uid
return <Table rowKey="uid" />;
// or
return <Table rowKey={(record) => record.uid} />;
FAQ
단일 페이지이거나 데이터가 없을 때 페이징을 숨기려면 어떻게 하나요? {#faq-hide-pagination}
pagination prop으로 hideOnSinglePage를 설정할 수 있습니다.
데이터를 필터링하면 Table이 첫 페이지로 돌아갑니다. {#faq-filter-to-first-page}
Table의 총 페이지 수는 보통 필터링 후 줄어들기 때문에, 현재 페이지가 필터링된 결과 범위를 벗어나면 기본적으로 첫 페이지로 돌아갑니다.
원격 서비스에서 데이터를 가져올 때 필터링 후 현재 페이지를 유지해야 할 수도 있습니다. 이 데모를 해결 방법으로 확인하세요.
또한 추가 매개변수의 액션을 사용해 언제 첫 페이지로 돌아갈지 결정할 수 있습니다.
Table 페이징에 크기 변경기가 표시되는 이유는 무엇인가요? {#faq-size-changer}
사용자 경험을 개선하기 위해 4.1.0부터 total > 50일 때 Pagination은 기본적으로 크기 변경기를 표시합니다. showSizeChanger=false로 이 기능을 비활성화할 수 있습니다.
상태가 바뀔 때 Table이 전체를 다시 렌더링하는 이유는 무엇인가요? {#faq-state-update-rerender}
Table은 columns.render에서 어떤 상태가 사용되는지 알 수 없으므로, 동기화 문제를 피하기 위해 항상 전체를 렌더링해야 합니다. column.shouldCellUpdate로 렌더링을 제어할 수 있습니다.
Table 성능 문제를 어떻게 해결해야 하나요? {#faq-table-performance}
React DevTools는 복잡한 테이블, 특히 행이나 셀이 많은 테이블을 프로파일링할 때 추가 오버헤드를 발생시킬 수 있습니다. 눈에 띄는 지연이 있다면 먼저 React DevTools를 비활성화하거나 깨끗한 브라우저 환경에서 테스트해 보세요. 일반 런타임에서도 성능 문제가 재현된다면 최소 재현 예제를 제공해 주시면 계속 조사하겠습니다.
마스크 레이아웃 위에 고정 컬럼 표시를 처리하는 방법은 무엇인가요? {#faq-fixed-column-zindex}
고정 컬럼은 z-index를 사용해 다른 컬럼 위에 놓습니다. 때로 고정 컬럼이 마스크 레이아웃 위로도 올라가는 것을 볼 수 있습니다. 마스크 레이아웃에 z-index를 설정해 해결할 수 있습니다.
Table Checkbox를 커스텀 렌더링하는 방법은 무엇인가요 (예: Tooltip 추가)? {#faq-custom-checkbox-render}
4.1.0부터 rowSelection.renderCell로 Table Checkbox를 커스텀 렌더링할 수 있습니다. Tooltip을 추가하려면 이 데모를 참고하세요.
virtual이 활성화될 때 components.body.wrapper 또는 components.body.row가 오류를 보고하는 이유는 무엇인가요? {#faq-virtual-wrapper-ref}
가상 테이블은 계산을 위해 자체 ref를 얻어야 하므로 React.forwardRef 래퍼를 사용해 dom에 ref를 전달해야 합니다. 이렇게:
const EditableRow = React.forwardRef<HTMLTableRowElement, EditableRowProps>(
({ index, ...props }, ref) => {
const [form] = Form.useForm();
return (
<Form form={form} component={false}>
<EditableContext.Provider value={form}>
<tr {...props} ref={ref} />
</EditableContext.Provider>
</Form>
);
},
);
고정된 행 높이와 세로 스크롤이 있는 시나리오에서는 다음 방법을 사용할 수 있습니다:
<Table
//@ts-ignore // This property is not exported, but it can be passed through to the internal virtual scrolling
listItemHeight={36} // This helps virtual scrolling calculate the height correctly, with each row fixed at 36px
/>