ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Material UI 创建主题化自定义组件:Slots、ownerState 与 useThemeProps 完整实践

Material UI 创建主题化自定义组件:Slots、ownerState 与 useThemeProps 完整实践 Material UI 创建主题化自定义组件Slots、ownerState 与 useThemeProps 完整实践【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Googles Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui本文基于 Material UI 官方文档「Creating themed components」讲解如何把你的自定义组件接入 Material UI 的主题系统使其像内置组件一样接受components配置styleOverrides、variants、defaultProps。通过本篇指南你将掌握用styledAPI 的name/slot参数定义组件插槽slots、用ownerState将 prop 传入插槽参与样式计算、用useThemeProps支持主题默认 props以及完整的 TypeScript 类型接入方案最终产出一个可跨项目复用的Stat统计组件模板。这套方案适用于在 Material UI 之上构建组件库的团队——组件一旦按此规范开发就可以在任何消费方的createTheme中直接定制。文档同时给出了一条务实建议如果你的组件只在单个项目中使用其实并不需要将组件接入主题系统只有需要跨项目可主题化themeable时才值得走完整流程。1. 组件插槽Slots让每个元素可被主题定位Material UI 的主题系统通过 theme 的 styleOverrides 和 variants 两个配置点来定制组件外观而这两个配置都以「组件名 插槽名」为定位坐标。因此第一步是为自定义组件定义插槽。以文档中的统计组件为例它由三个插槽组成root组件的容器元素value统计数值unit统计单位的说明文字。官方建议无论插槽叫什么名字最外层容器元素统一命名为root以保持与库内其余组件的一致性。使用styledAPI 并传入name组件名和slot插槽名两个选项来创建插槽import * as React from react; import { styled } from mui/material/styles; const StatRoot styled(div, { name: MuiStat, // The component name slot: root, // The slot name })(({ theme }) ({ display: flex, flexDirection: column, gap: theme.spacing(0.5), padding: theme.spacing(3, 4), backgroundColor: theme.palette.background.paper, borderRadius: theme.shape.borderRadius, boxShadow: theme.shadows[2], letterSpacing: -0.025em, fontWeight: 600, ...theme.applyStyles(dark, { backgroundColor: inherit, }), })); const StatValue styled(div, { name: MuiStat, slot: value, })(({ theme }) ({ ...theme.typography.h3, })); const StatUnit styled(div, { name: MuiStat, slot: unit, })(({ theme }) ({ ...theme.typography.body2, color: theme.palette.text.secondary, }));这三个styled组件都声明name: MuiStat意味着它们共享同一个主题定制命名空间仅靠slot区分具体元素。从源码结构看name/slot参数的处理位于 createStyled 实现styled工厂函数接收tag与inputOptions从中解析出组件名与插槽名。此外仓库中的 完整模板 还在插槽样式里直接使用了variants数组variant: outlined时加边框、去掉阴影这与文档分步指南中通过ownerState条件展开的方式是等价的两种写法——variants写法可以让样式随主题被解析并支持 CSS 层叠而条件展开写法对内部状态更灵活。2. 组装组件并应用主题有了插槽之后用React.forwardRef把三个插槽组装成完整组件// /path/to/Stat.js import * as React from react; const StatRoot styled(div, { name: MuiStat, slot: root, })(…); const StatValue styled(div, { name: MuiStat, slot: value, })(…); const StatUnit styled(div, { name: MuiStat, slot: unit, })(…); const Stat React.forwardRef(function Stat(props, ref) { const { value, unit, ...other } props; return ( StatRoot ref{ref} {...other} StatValue{value}/StatValue StatUnit{unit}/StatUnit /StatRoot ); }); export default Stat;此时消费方就可以像定制内置组件一样在createTheme中按「组件名 → 插槽名」定位样式import { createTheme } from mui/material/styles; const theme createTheme({ components: { // the component name defined in the name parameter // of the styled API MuiStat: { styleOverrides: { // the slot name defined in the slot and overridesResolver parameters // of the styled API root: { backgroundColor: #121212, }, value: { color: #fff, }, unit: { color: #888, }, }, }, }, });关键在于styleOverrides的键必须与styledAPI 中slot参数一一对应MuiStat则对应name参数——这就是第 1 步声明name/slot的意义所在它建立了主题配置与 DOM 元素之间的映射。3. 用 ownerState 让插槽感知组件 prop当组件有variant这类需要影响内部插槽样式的 prop 时直接解构传递会导致插槽无法读取到它它不会被 spread 到需要它的地方。官方方案是把需要样式化的 prop 和内部状态封装进ownerState对象作为 prop 传给每个插槽。ownerState是一个特殊名称styledAPI 不会把它透传到 DOM因此它只参与样式计算、不污染渲染结果。先给Stat增加variantpropconst Stat React.forwardRef(function Stat(props, ref) { const { value, unit, variant, ...other } props; const ownerState { ...props, variant }; return ( - StatRoot ref{ref} {...other} - StatValue{value}/StatValue - StatUnit{unit}/StatUnit - /StatRoot StatRoot ref{ref} ownerState{ownerState} {...other} StatValue ownerState{ownerState}{value}/StatValue StatUnit ownerState{ownerState}{unit}/StatUnit /StatRoot ); });然后在插槽内读取ownerState基于variant计算样式const StatRoot styled(div, { name: MuiStat, slot: root, - })(({ theme }) ({ })(({ theme, ownerState }) ({ display: flex, flexDirection: column, gap: theme.spacing(0.5), padding: theme.spacing(3, 4), backgroundColor: theme.palette.background.paper, borderRadius: theme.shape.borderRadius, boxShadow: theme.shadows[2], letterSpacing: -0.025em, fontWeight: 600, ...theme.applyStyles(dark, { backgroundColor: inherit, }), ...ownerState.variant outlined { border: 2px solid ${theme.palette.divider}, }, }));「ownerState 不会 spread 到 DOM」这一点在源码中有明确依据createStyled 中的属性过滤函数会显式排除ownerState、theme、sx、as这几个属性保证它们只留在样式层。另外从源码结构看processStyleVariants同文件在匹配variants配置时会合并props与props.ownerState后再比较variant.props——这解释了为什么在模板文件里variants的props: { variant: outlined }能直接命中variantprop也印证了ownerState是插槽样式访问组件状态的标准通道。4. 支持主题默认 propsuseThemeProps不同消费项目可能希望以主题为单位定制组件的默认 props例如全局默认variant: outlined。这一步必须使用useThemePropsAPI import { useThemeProps } from mui/material/styles; - const Stat React.forwardRef(function Stat(props, ref) { const Stat React.forwardRef(function Stat(inProps, ref) { const props useThemeProps({ props: inProps, name: MuiStat }); const { value, unit, ...other } props; return ( StatRoot ref{ref} {...other} StatValue{value}/StatValue StatUnit{unit}/StatUnit /StatRoot ); });name: MuiStat必须与styled的name保持一致这样主题中该组件条目下的defaultProps才会被合入。之后消费方即可在主题里覆盖默认值import { createTheme } from mui/material/styles; const theme createTheme({ components: { MuiStat: { defaultProps: { variant: outlined, }, }, }, });从实现看mui/material 的 useThemeProps 是对 mui-system 版本 的封装它先通过useTheme取到当前主题支持themeId命名空间再调用getThemeProps({ theme, name, props })完成「主题defaultProps打底 传入 props 覆盖」的合并。这也意味着useThemeProps必须在forwardRef回调内部调用以保证始终读取当前主题上下文。5. TypeScript 接入Props、ownerState 与模块声明使用 TypeScript 时需要为组件 props 与 ownerState 分别定义接口interface StatProps { value: number | string; unit: string; variant?: outlined; } interface StatOwnerState extends StatProps { // …key value pairs for the internal state that you want to style the slot // but dont want to expose to the users }StatOwnerState在StatProps基础上扩展——既可以复用对外的 prop也可以加入「只想用于插槽样式、不想暴露给用户」的内部状态键值对。然后把类型注入插槽与组件const StatRoot styled(div, { name: MuiStat, slot: root, }){ ownerState: StatOwnerState }(({ theme, ownerState }) ({ display: flex, flexDirection: column, gap: theme.spacing(0.5), padding: theme.spacing(3, 4), backgroundColor: theme.palette.background.paper, borderRadius: theme.shape.borderRadius, boxShadow: theme.shadows[2], letterSpacing: -0.025em, fontWeight: 600, ...theme.applyStyles(dark, { backgroundColor: inherit, }), // typed-safe access to the variant prop ...(ownerState.variant outlined { border: 2px solid ${theme.palette.divider}, boxShadow: none, }), })); // …do the same for other slots const Stat React.forwardRefHTMLDivElement, StatProps(function Stat(inProps, ref) { const props useThemeProps({ props: inProps, name: MuiStat }); const { value, unit, variant, ...other } props; const ownerState { ...props, variant }; return ( StatRoot ref{ref} ownerState{ownerState} {...other} StatValue ownerState{ownerState}{value}/StatValue StatUnit ownerState{ownerState}{unit}/StatUnit /StatRoot ); });注意styled调用后的泛型参数{ ownerState: StatOwnerState }让插槽内对ownerState.variant的访问获得类型安全组件本体的forwardRefHTMLDivElement, StatProps则保证 ref 与 props 的类型正确。最后一步是把Stat注册进主题类型系统使消费方在createTheme中写MuiStat配置时能获得完整的类型提示与校验import { ComponentsOverrides, ComponentsVariants, Theme as MuiTheme, } from mui/material/styles; import { StatProps } from path/to/Stat; type Theme OmitMuiTheme, components; declare module mui/material/styles { interface ComponentNameToClassKey { MuiStat: root | value | unit; } interface ComponentsPropsList { MuiStat: PartialStatProps; } interface Components { MuiStat?: { defaultProps?: ComponentsPropsList[MuiStat]; styleOverrides?: ComponentsOverridesTheme[MuiStat]; variants?: ComponentsVariants[MuiStat]; }; } }三处声明各司其职ComponentNameToClassKey声明该组件拥有的插槽集合对应生成的 class 键ComponentsPropsList声明defaultProps的合法取值Components接口把MuiStat正式加入components配置类型使其styleOverrides/variants均受类型约束。6. 完整模板与验证上述四步的完整产物即仓库内的 Stat 组件模板JS 版 与 TypeScript 版可直接作为自建主题化组件的起点。模板要点回顾三个插槽统一声明name: MuiStat分别使用slot: root | value | unit插槽样式中直接携带variants数组将variant: outlined映射为边框样式组件内先经useThemeProps({ props: inProps, name: MuiStat })合并主题默认 props再构造ownerState并下发到每个插槽JS 版附带propTypes声明value: number | string、unit: string、variant: outlined便于运行时校验。最终效果消费方既可以在主题里写styleOverrides.root/value/unit覆盖外观也可以写defaultProps.variant改变默认行为——自定义组件在 API 层面与内置组件完全同构。7. 适用前提与小结适用前提方案基于mui/material/styles导出的styled、createTheme、useThemeProps适用于 Material UI 当前的主题系统单项目内使用的简单组件可跳过整条主题化流程。四个关键约定name对应主题components键名、slot对应styleOverrides键名、ownerState承载插槽可见的组件状态且不透传 DOM、useThemeProps的name必须与styled的name一致。可继续深入的路径styled API 源码 中name/slot/overridesResolver的解析逻辑、useThemeProps 实现 的主题合并流程以及官方 Theme components 文档 中styleOverrides与variants的完整配置格式。【免费下载链接】material-uiMaterial UI: Comprehensive React component library that implements Googles Material Design. Free forever.项目地址: https://gitcode.com/GitHub_Trending/ma/material-ui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进