React+TypeScript封装MUI MenuItem自定义组件各类报错的解决方法
报错根因拆解
- 类型不匹配导致TS编译错误:MUI MenuItem 默认渲染根节点为
<li>,对应可接收的ref类型是HTMLLIElement;只有开启button属性模式时,根节点才会渲染为<button>,对应HTMLButtonElement。原代码把ref类型声明为HTMLButtonElement,和MenuItem实际承接的ref类型不兼容,直接触发TS的赋值检查报错。 - 组件封装遗漏核心步骤:仅定义了
ForwardRefRenderFunction类型的渲染函数,没有通过React.forwardRef将渲染函数包装为可承接外部ref的正式组件,导出的本质还是普通函数组件。而MUI Select 在渲染菜单项时,会通过cloneElement给每个直接子菜单项绑定内部管理的ref(用于键盘导航、滚动定位等逻辑),普通函数组件无法承接这类ref,自然触发"Function components cannot be given refs"的警告。 - 强转
any的运行时副作用:把泛型改成any跳过类型检查后,由于组件本身没有ref透传能力,Select内部往组件上挂载ref时,会尝试在不可扩展的函数对象上添加current属性,最终触发Cannot add property current, object is not extensible的运行时崩溃。
正确的 MenuItem 封装实现
完整代码需要对齐类型匹配、完成forwardRef包装两个核心要求:
import { forwardRef } from 'react'; import MenuItem, { MenuItemProps } from '@mui/material/MenuItem'; // 对齐MenuItem的props泛型,默认渲染li元素 type DMSMenuItemProps = MenuItemProps<"li", { button?: true }>; // 泛型参数第一个匹配ref指向的DOM类型HTMLLIElement,第二个匹配组件Props类型 const DMSMenuItem = forwardRef<HTMLLIElement, DMSMenuItemProps>((props, ref) => { return <MenuItem ref={ref} {...props} />; }); // 补充组件名,方便DevTools调试 DMSMenuItem.displayName = 'DMSMenuItem'; export default DMSMenuItem;
如果需要支持button模式下的按钮类型MenuItem,可以将封装调整为泛型组件适配不同根DOM类型,不需要强转any:
// 支持button模式的泛型封装版本 const DMSMenuItem = forwardRef<HTMLElement, MenuItemProps & { button?: true }>((props, ref) => { return <MenuItem ref={ref} {...props} />; });
配套的 Select 组件封装优化
原DMSSelect 是普通函数组件,如果后续需要接收外部传入的ref(比如手动触发Select焦点、获取内部DOM元素),也需要同步做forwardRef封装,避免同类报错:
import { forwardRef } from 'react'; import { Select, SelectProps } from '@mui/material/Select'; import FormControl from '@mui/material/FormControl'; import InputLabel from '@mui/material/InputLabel'; import FormHelperText from '@mui/material/FormHelperText'; interface IDMSSelectProps extends SelectProps { hint?: string; label: string; } const DMSSelect = forwardRef<HTMLDivElement, IDMSSelectProps>((props, ref) => { // 解构出自定义属性,避免透传到原生DOM触发非标准属性警告 const { error, label, children, hint, ...restSelectProps } = props; return ( <FormControl error={error} fullWidth> <InputLabel>{label}</InputLabel> <Select ref={ref} fullWidth {...restSelectProps}> {children} </Select> <FormHelperText>{hint}</FormHelperText> </FormControl> ); }); DMSSelect.displayName = 'DMSSelect'; export default DMSSelect;
封装注意事项
- 不要直接导出
ForwardRefRenderFunction类型的原始函数:该类型仅用于标注传入forwardRef的渲染函数的参数结构,本身不具备承接ref的能力,必须经过React.forwardRef包装后导出才能在作为Select子组件时正常工作。 - 避免用
any跳过类型检查:类型不匹配时优先对齐MUI组件自身的类型定义,不要通过强转any规避检查,否则会隐藏运行时的ref透传问题。 - 透传属性时要解构出自定义扩展的props,不要把非MUI组件要求的属性(比如
hint)直接spread给原生DOM,否则会触发React的非标准DOM属性警告。
内容的提问来源于stack exchange,提问作者Pierre-luc Des.
相关产品推荐
相关产品推荐

