Custom Ant Design Select Component with Integrated Table and Dumi Documentation Guide
This article explains how to create custom Ant Design select components that display additional information via integrated tables, covering form implementation, basic and advanced versions with infinite scrolling, handling Ant Design bugs, and documenting the components using Dumi, complete with code examples.
Introduction
Our company uses the Ant Design UI framework. The default Select component only shows a name, which can be ambiguous when names repeat (e.g., people with the same name). To let users distinguish items by employee ID or role, we built two custom component sets and share the implementation here.
Form Implementation
Background
In Ant Design, Select is often used together with the Form component. By reading the Ant Design Form source code, we created a simple Form component to demonstrate how values are injected into child components.
When an Input component is wrapped with FormItem , the value is automatically stored in the form and can be accessed via the form instance.
Implementation Idea
The core of the Form component is that FormItem uses React.cloneElement to clone its child and inject an onChange handler that writes the value back to the form context.
FormItem Implementation
import React from 'react';
import { FormContext } from './context';
import { useMemo } from 'react';
function FormItem({ label, name, children }) {
const { values, setValues } = React.useContext(FormContext);
const childNodes = useMemo(() => React.Children.map(children, (child) => {
return React.cloneElement(child, {
...child.props,
onChange: (value) => {
setValues({
...values,
[name]: value.target.value,
})
},
value: values[name]
})
}), [children, values])
return (
<>
{label}:
{childNodes}
)
}
export default FormItem;Form Component Implementation
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { FormContext } from './context';
function Form({ children, onValuesChange }, ref) {
const [values, setValues] = useState({});
useEffect(() => {
onValuesChange && onValuesChange(values);
}, [values]);
useImperativeHandle(ref, () => ({
getValues: () => values,
setValues: (v) => setValues(v)
}), [values]);
return (
{children}
)
}
export default forwardRef(Form);Summary
If you want to wrap a custom input so that it works with Ant Design Form , you only need to expose onChange and value props.
Basic Version
Background
To solve the duplicate‑name problem, we first designed a selection scheme where clicking the select opens a modal containing a table. The table can show more fields, and the user selects a row.
Reusable Table Component
Ant Design's table is powerful but requires a lot of boilerplate (request, pagination, search). We wrapped it into a reusable component.
import React, { useState, useImperativeHandle, forwardRef, useEffect, useMemo, useRef } from "react";
import { Table, Alert, Space } from 'antd';
import queryString from 'query-string';
import TableEllipsisCell from './table-ellipsis-cell';
function BaseTable({ fetchDataHandle, url, method = 'GET', dataSource: dataSourceProps, allowChecked, onCheckedChange, selectedRows, ...rest }, ref) {
const [dataSource, setDataSource] = useState(dataSourceProps);
const [loading, setLoading] = useState(true);
const [pagination, setPagination] = useState({});
const [searchParams, setSearchParams] = useState({});
const dataMap = useRef({});
const onSelectChange = (selectedRowKeys) => {
if (onCheckedChange) {
onCheckedChange(selectedRowKeys.map(key => dataMap.current[key]).filter(o => o));
}
};
const [rowSelection, setRowSelection] = useState(() => ({
selectedRowKeys: [],
onChange: onSelectChange,
columnWidth: 40,
preserveSelectedRowKeys: true,
fixed: true,
}));
// expose search method
useImperativeHandle(ref, () => ({
search: async (params) => {
setPagination(prev => ({ ...prev, current: 1 }));
setSearchParams(params);
}
}), []);
const getData = async (pagination, searchParams) => {
let requestMethod = fetchDataHandle;
if (!requestMethod && url) {
requestMethod = async (params) => {
return await window.fetch([url, queryString.stringify(params)].join(url.includes('?') ? '&' : '?'), { method })
.then(res => res.json())
.then(data => data);
}
}
if (requestMethod && dataSourceProps === undefined) {
setLoading(true);
const { list, total } = await requestMethod({ pageSize: pagination.pageSize, page: pagination.current, ...searchParams })
.then(d => d).catch(() => null);
if (list) {
setDataSource(list);
setLoading(false);
setPagination({ ...pagination, total });
}
} else if (dataSourceProps) {
setLoading(false);
}
};
const tableChange = (pagination) => setPagination(pagination);
useEffect(() => {
if (allowChecked) {
setRowSelection(prev => ({ ...prev, selectedRowKeys: (selectedRows || []).map(o => o[rest.rowKey || 'id']) }));
}
}, [selectedRows]);
useEffect(() => {
getData(pagination, searchParams);
}, [pagination.current, pagination.pageSize, searchParams]);
useEffect(() => {
dataMap.current = { ...dataMap.current, ...(dataSource || []).reduce((prev, cur) => { prev[cur[rest.rowKey]] = cur; return prev; }, {}) };
}, [dataSource]);
const columns = useMemo(() => (rest.columns || []).map(item => {
if (!item.ellipsis) return item;
return { ...item, render: (text, record, index) => (
) };
}), [rest.columns]);
return (
{allowChecked && (
onSelectChange([])}>取消选择
)} />)}
);
}
export default BaseTable;The table component is simple; note that Ant Design tables support ellipsis but not tooltip. We added a custom cell component to show a tooltip when the text overflows.
Ellipsis Cell Component
import React from "react";
import { Tooltip } from 'antd';
import { useRef, useState } from 'react';
function TableEllipsisCell({ value }) {
const boxRef = useRef();
const [open, setOpen] = useState(false);
const onOpenChange = (flag) => {
if (flag) {
if (boxRef.current.offsetWidth < boxRef.current.scrollWidth) setOpen(true);
} else {
setOpen(false);
}
};
return (
{value}
);
}
export default TableEllipsisCell;Search Form Component
import React from "react";
import { Form, Input, Button, Row, Col, Space } from 'antd';
function SearchForm({ items, onSearch }) {
const [form] = Form.useForm();
const onFinish = (values) => { onSearch && onSearch(values); };
const renderFormElement = () => (
);
return (
{(items || []).map(item => (
{renderFormElement()}
))}
搜索
{ form.resetFields(); form.submit(); }}>重置
); }
export default SearchForm;Table with Search Component
import React, { useRef, useMemo } from "react";
import BaseTable from './table';
import SearchForm from './search-form';
function ProTable({ columns, allowChecked, ...tableProps }) {
const tableRef = useRef();
const searchFormItems = useMemo(() => (columns || []).filter(item => item.search).map(item => ({ name: item.dataIndex, label: item.title })), [columns]);
return (<>
{ tableRef.current.search(values); }} items={searchFormItems} />
);
}
export default ProTable;Testing
Images show the modal and table behavior. The phone‑number column is intentionally short to test the tooltip.
Advanced Version
Background
After using the basic version, users found the interaction a bit cumbersome. The advanced version embeds the table directly inside the dropdown, removes pagination and search UI, and uses infinite scrolling to load more data.
Scrollable Table Component
We add a loading row at the bottom and use IntersectionObserver to detect when it appears in the viewport, then load the next page.
import { useState, useEffect, useMemo } from 'react';
const useIntersectionObserver = (domRef) => { const [intersecting, setIntersecting] = useState(false); const intersectionObserver = useMemo(() => new IntersectionObserver(entries => { setIntersecting(entries.some(item => item.isIntersecting)); }), []); useEffect(() => () => { intersectionObserver.disconnect(); }, []); useEffect(() => { if (domRef.current) { intersectionObserver.observe(domRef.current); } }, [domRef.current]); return { intersecting, disconnect: () => intersectionObserver.disconnect() }; };
export default useIntersectionObserver;The scrollable table is similar to the basic table but disables Ant Design pagination and adds a summary row that contains the loading indicator.
import React, { useState, useImperativeHandle, forwardRef, useEffect, useMemo, useRef } from "react";
import { Table, Spin } from 'antd';
import queryString from 'query-string';
import TableEllipsisCell from './table-ellipsis-cell';
import useIntersectionObserver from '../useIntersectionObserver';
function ScrollTable({ fetchDataHandle, url, method = 'GET', dataSource: dataSourceProps, allowChecked, onCheckedChange, selectedRows, summaryWidth, hiddenLoading, ...rest }, ref) { /* similar logic, but pagination is driven by IntersectionObserver */ }
export default forwardRef(ScrollTable);Dropdown Component Integration
We create a TableSelect component that renders an Ant Design Select whose dropdown is replaced by a modal containing the table. The component converts the selected rows into the labelInValue format expected by Select .
import React, { useState } from "react";
import { Select, Tooltip } from 'antd';
import { TableOutlined } from '@ant-design/icons';
import TableSelectModal from './table-select-modal';
function TableSelect({ onChange, value, mode, tableColumns, labelKey = 'label', rowKey = 'id', modalTitle, url, tableFetchDataHandle, tableProps }) { const [open, setOpen] = useState(false); const getValue = () => { if (mode === 'multiple') { return value?.map(item => ({ value: item?.[rowKey], label: item?.[labelKey] })); } return { value: value?.[rowKey], label: value?.[labelKey] }; }; return (<>
setOpen(true)} />} value={getValue()} onClick={() => setOpen(true)} mode={mode} onDeselect={(v) => { if (mode === 'multiple') { onChange(value.filter(item => item[rowKey] !== v.value)); } }} labelInValue maxTagCount="responsive" maxTagPlaceholder={omittedValues => (
o.label).join(',')}>
+ {omittedValues.length}
)} />
); }
export default TableSelect;We also discuss a bug in Ant Design where a table inside a modal with virtual scrolling miscalculates dimensions during the opening animation. The new afterOpenChange event solves this.
Documentation with Dumi
Initialize a Dumi Project
Run npx create-dumi , choose the "React Library" template, install dependencies, and start the dev server.
Install a Theme
Install dumi-theme-vite (or any other theme) to get a nicer look.
Write Component Docs
Place component source files under src . Write a markdown file ( index.md ) for each component, and export the component in index.tsx . Dumi will render the examples directly in the docs.
Example
The article shows how to move the custom select components into a Dumi library, write the markdown documentation, and see the live examples.
Conclusion
The guide provides a complete workflow: building a reusable Ant Design select with table integration, handling common pitfalls, and publishing the component with proper documentation using Dumi. Publishing to npm and deploying the docs site are left for future posts.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.