首页
/ Material UI 组件 DOM 结构覆盖实战:component、slots 与 slotProps 三大 API 详解

Material UI 组件 DOM 结构覆盖实战:component、slots 与 slotProps 三大 API 详解

2026-09-06 10:59:32作者:俞予舒Fleming

本文基于 Material UI 官方定制指南「Overriding component structure」展开,系统讲解如何覆盖 Material UI 组件默认渲染的 DOM 结构:从 v6 版本 API 演进的背景,到 componentslotsslotProps 三个核心 props 的心智模型与用法,并结合开源仓库中 useSlot 工具函数Autocomplete 源码 的实现,深入解析结构覆盖在底层的真实调用链与属性合并规则,帮助你既会用 API,也知其所以然。

背景:为什么要从 v6 开始讲结构覆盖

Material UI 组件在设计之初就面向尽可能广泛的用例,但有时你需要改变某个组件在 DOM 中的渲染方式。要理解这件事,需要先了解 API 设计的历史演进,并建立对组件结构的心智模型。

在 Material UI v6 之前,库中大多数组件的结构是无法覆盖的。部分组件提供过 *Props 形式的 props(例如把某个子节点的 props 透传出去),但这种模式在整个库中应用并不一致。v6 版本将这些 props 标记为弃用,转而统一为 slotsslotProps 两个 props:它们提供了对组件结构更细粒度的控制,并让 API 在整个库范围内保持一致。

心智模型:组件结构由「slot(插槽)」决定

一个组件的结构,由填充该组件 slot(插槽) 的元素决定。slot 最常见的填充物是 HTML 标签,但也可以是 React 组件。

  • 所有组件都包含一个 root slot,它定义了组件在 DOM 树中的主节点;
  • 更复杂的组件还会包含若干 interior slots(内部插槽),命名自它们所代表的元素。

要查看某个组件可用哪些 slot,请查阅该组件 API 文档中的 slots 小节。

所有**非工具类(non-utility)**的 Material UI 组件都接受两个用于覆盖其渲染 HTML 结构的 props:

Prop 作用
component 覆盖 root slot
slots 替换任意内部插槽(如果存在),也可以替换 root

此外,可以通过 slotProps 向内部插槽传递自定义 props。

根插槽(root slot)与 component prop

root slot 代表组件最外层的元素,默认由一个带有合适 HTML 标签的 styled component 填充。例如,Button 组件的 root slot 是一个 <button> 元素;Button 只有 root slot,而更复杂的组件还会有额外的内部插槽。

使用 component prop 可以覆盖组件的 root slot。官方示例演示了如何用 <a> 替换 Button 的 <button> 标签,创建一个链接按钮:

import Button from '@mui/material/Button';

export default function OverridingRootSlot() {
  return (
    <Button component="a" href="https://mui.com/about/" target="_blank">
      About us
    </Button>
  );
}

完整示例可见 OverridingRootSlot.js(另有 TypeScript 版本)。

注意hreftargetrel 这些 props 是 <a> 标签特有的。使用 component prop 时,务必补上与目标元素对应的属性。

内部插槽(interior slots)与 slots prop

复杂组件由 root 之外的一组内部插槽组成,这些插槽通常(但不一定)嵌套在 root 之内。

以 Autocomplete 为例:它的结构是 root 层的一个 <div>,内部又包含若干以其元素命名的内部插槽:inputstartDecoratorendDecoratorclearIndicatorpopupIndicator,等等。

使用 slots prop 可以替换组件的内部插槽。官方示例演示了如何替换 Autocomplete 的 popper 插槽,从而移除弹出(popup)功能:

import Box from '@mui/material/Box';
import PropTypes from 'prop-types';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';

function PopperComponent(props) {
  const { disablePortal, anchorEl, open, ...other } = props;
  return <div {...other} />;
}

PopperComponent.propTypes = {
  anchorEl: PropTypes.any,
  disablePortal: PropTypes.bool,
  open: PropTypes.bool.isRequired,
};

export default function OverridingInternalSlot() {
  return (
    <Box sx={{ display: 'flex', flexDirection: 'column', width: 320, minHeight: 220 }}>
      <Autocomplete
        open
        options={['🆘 Need help', '✨ Improvement', '🚀 New feature', '🐛 Bug fix']}
        renderInput={(params) => <TextField {...params} />}
        slots={{
          popper: PopperComponent,
        }}
      />
    </Box>
  );
}

完整示例可见 OverridingInternalSlot.js(含 TypeScript 版本)。

对照源码可以看到这套机制的落实方式:在 Autocomplete.js 中,组件通过 useSlot('root', {...})useSlot('popper', {...})useSlot('paper', {...}) 等调用逐个创建插槽,其中 PopperSlot 的默认 elementType 就是 Popper(第 635 行附近),additionalProps 中携带了 anchorElopendisablePortal 等内部依赖——这就是为什么替换 popper 插槽时,自定义的 PopperComponent 必须自行解构并忽略这些内部 props(openanchorEldisablePortal),只把剩下的 ...other(含 className)透传给原生元素。

slotProps:向插槽传递自定义 props

slotProps 是一个对象,包含组件内所有插槽的 props,用于定义传给内部插槽的额外自定义 props。例如,给 Autocomplete 的 popper 插槽加上一个自定义 data-testid

<Autocomplete slotProps={{ popper: { 'data-testid': 'my-popper' } }} />

回调形式的 slotProps

每个 slot 的 props 也可以是一个回调函数,接收组件的 ownerState 并返回该插槽的 props。当你需要插槽 props 依赖组件的 props 或内部状态时使用这种形式:

<Popover
  open={open}
  slotProps={{
    paper: (ownerState) => ({
      elevation: ownerState.open ? 8 : 0,
    }),
  }}
/>

从源码看,这个回调形式是被统一支持的:useSlot.ts 中会先执行 resolveComponentProps(slotProps[name], ownerState),把「对象」或「接收 ownerState 的回调」统一解析为一个纯对象,再进入合并流程。类型定义(同文件第 42-48 行)也明确 slotProps 的每个键可以是 ExternalSlotProps | ((ownerState: OwnerState) => ExternalSlotProps)

额外 props 会传播到 root slot

放在主组件上的所有额外 props,也会传播进 root slot(就像它们被写进了 slotProps.root 一样)。下面两个示例是等价的:

<Badge id="badge1">
<Badge slotProps={{ root: { id: 'badge1' } }}>

这一点在 useSlot.ts 的实现中有直接证据:mergeSlotProps 的入参中 externalForwardedProps: name === 'root' ? other : undefined——只有当插槽名为 root 时,组件上的其余属性(other)才会被合并进去,其他内部插槽则忽略这些额外 props。

优先级规则:如果 slotProps.root 与额外 props 的键相同但值不同,slotProps.root 中的 props 优先。这一规则不适用于 classesstyle——它们会被合并(merge),而不是覆盖。

TypeScript 类型安全

slotProps 的类型不会基于你自定义的 slots 动态变化。因此,当自定义插槽与默认插槽的类型不同时,你需要对类型做断言以避免 TypeScript 报错,并使用 satisfies(TypeScript 4.9 引入)来保证自定义插槽自身的类型安全。

例如,用 Next.js 的 Image 组件自定义 Avatar 的 img 插槽:

import Image, { ImageProps } from 'next/image';
import Avatar, { AvatarProps } from '@mui/material/Avatar';

<Avatar
  slots={{
    img: Image,
  }}
  slotProps={
    {
      img: {
        src: 'https://example.com/image.jpg',
        alt: 'Image',
        width: 40,
        height: 40,
        blurDataURL: 'data:image/png;base64',
      } satisfies ImageProps,
    } as AvatarProps['slotProps']
  }
/>;

要点拆解:satisfies ImageProps 保证 srcaltwidthheightblurDataURL 符合 Next.js Image 的 prop 约束;外层的 as AvatarProps['slotProps'] 则把整体断言回 Avatar 期望的 slotProps 类型,因为 AvatarProps['slotProps'] 并不知道 img 槽已被换成了 next/imageImage

最佳实践

  • 需要覆盖元素但保留插槽样式时,使用 componentslotProps.{slot}.component prop。
  • 需要用自定义组件整体替换插槽的样式与功能时,才使用 slots prop。

component 覆盖,可以直接把目标元素的属性挂到根上。例如把 Button 的 root 覆盖为 <li> 标签时,你可以直接把 <li> 的属性 value 加到组件上;如果改用 slots.root,则必须把这个属性放进 slotProps.root 对象中,否则 TypeScript 会报错。

component 之所以能「换元素而保样式」,其机制在 useSlot.ts 中体现得很清楚:解析出最终要替换的 LeafComponent 后,代码并不直接把它作为渲染节点,而是转成 styled 组件的 as prop(第 143-146 行)。也就是说 <Button component="a"> 实际是把 styled button 重新指向 <a> 渲染,Button 的全部样式规则原样保留。

源码中还有一处值得注意的分支:shouldForwardComponentProp 标志(第 91-97 行注释与第 147-150 行实现)。当插槽的 elementType 本身是另一个 Material UI 组件的 styled component(例如 Autocomplete 的 paper 插槽默认元素是 Paper)时,若把 component 转成 asas 会替换掉 Paper 这个组件本身、导致 Paper 的样式丢失;此时 useSlot 会把 component 通过 component 键继续向下转发给内层组件(Autocomplete.js 中 clearIndicatorpopupIndicator 等插槽就显式设置了 shouldForwardComponentProp: true),从而保证「换元素不换皮」。

  • 注意渲染出的 DOM 结构:覆盖复杂组件的插槽时,偏离默认结构过远容易破坏语义化、可访问的 HTML 规则——例如无意中把块级元素嵌套进行内元素。

小结

Material UI v6 之后,「结构即插槽」是理解组件渲染的核心模型:component 管 root slot 的换元素(保样式),slots 管任意插槽的换组件(连样式一起换),slotProps 管给插槽加 props(对象或回调,root 插槽还享受额外 props 自动传播与 slotProps.root 优先的合并规则)。这三个 API 的底层统一落在 useSlot 中:slots[name] || initialElementType 决定最终渲染的组件,resolveComponentProps 处理回调形式的 slotProps,mergeSlotProps 完成内部/外部 props 与 classNamestyle 的合并,ascomponent 的转换则守护了样式不丢失的底线。掌握这套模型后,你既能在 Button 上做「链接按钮」这类简单替换,也能在 Autocomplete、Popover 这类复杂组件上做精细的结构定制,而不破坏组件原有的外观与可访问性。

登录后查看全文
热门项目推荐
相关项目推荐