Frontend Development 26 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Custom Ant Design Select Component with Integrated Table and Dumi Documentation Guide

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.

frontendreactdocumentationAnt DesigndumitableCustom Select
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.