处理 React Portal 中的 TypeScript 类型错误
问题描述
在使用 React Portal 时,经常会遇到 TypeScript 类型错误:
Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Element'.
Type 'null' is not assignable to type 'Element'.ts(2345)
这个错误通常出现在类似下面的代码中:
<body>
<div id="portal"></div>
<div id="root"></div>
</body>
import React from 'react';
const portalDiv = document.getElementById('portal');
function Portal1(props) {
return ReactDOM.createPortal(
<div>
{props.children}
<div/>,
portalDiv); // 这里出现错误
}
export default Portal1;
错误原因分析
核心问题
document.getElementById()
方法的返回类型是 HTMLElement | null
,而 ReactDOM.createPortal()
方法的第二个参数要求必须是 Element
类型(不能为 null)。
当 TypeScript 的 strictNullChecks
选项启用时(推荐启用),它会在编译时检查可能的 null 值,从而防止运行时错误。
解决方案
方法一:使用类型断言(推荐)
如果你确定元素一定存在,可以使用类型断言:
const portalDiv = document.getElementById('portal') as HTMLElement;
或者使用非空断言操作符:
const portalDiv = document.getElementById('portal')!;
注意事项
- 只在你能 100% 确定元素存在时使用这种方法
- 如果元素可能不存在,应该添加适当的错误处理
方法二:添加 null 检查
更安全的方法是在使用前检查元素是否存在:
const portalDiv = document.getElementById('portal');
if (!portalDiv) {
throw new Error("找不到 #portal 元素");
}
// 现在 TypeScript 知道 portalDiv 不会是 null
function Portal1(props) {
return ReactDOM.createPortal(
<div>
{props.children}
</div>,
portalDiv
);
}
方法三:条件渲染
如果元素可能不存在,可以在组件内部进行条件渲染:
function Portal1({ children }) {
const portalDiv = document.getElementById('portal');
return portalDiv
? ReactDOM.createPortal(<>{children}</>, portalDiv)
: null;
}
方法四:React Hooks 中的处理
在函数组件中使用 useEffect 处理 Portal:
import React, { useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
type Props = {
children: React.ReactNode;
};
const ModalPortal: React.FC<Props> = ({ children }) => {
const portalContainer = useRef(document.createElement('div'));
const portalRoot = useRef(document.getElementById('portal'));
useEffect(() => {
const currentPortalRoot = portalRoot.current;
const currentContainer = portalContainer.current;
if (currentPortalRoot && currentContainer) {
currentPortalRoot.appendChild(currentContainer);
return () => {
currentPortalRoot.removeChild(currentContainer);
};
}
}, []);
if (!portalRoot.current) {
return null;
}
return ReactDOM.createPortal(children, portalContainer.current);
};
export default ModalPortal;
最佳实践建议
启用严格模式:不要禁用
strictNullChecks
,这是 TypeScript 的重要安全特性防御性编程:总是假设 DOM 元素可能不存在,特别是在组件挂载的早期阶段
错误边界:对于关键的 Portal 容器,添加适当的错误处理
使用 React Refs:对于动态内容,考虑使用 React ref 而不是直接 DOM 查询
常见使用场景
const Modal = ({ isOpen, children }) => {
const modalRoot = document.getElementById('modal-root') as HTMLElement;
if (!isOpen) return null;
return ReactDOM.createPortal(
<div className="modal-overlay">
<div className="modal-content">
{children}
</div>
</div>,
modalRoot
);
};
const Tooltip = ({ targetId, content }) => {
const targetElement = document.getElementById(targetId);
const tooltipRoot = document.getElementById('tooltip-root') as HTMLElement;
if (!targetElement) return null;
const rect = targetElement.getBoundingClientRect();
return ReactDOM.createPortal(
<div
className="tooltip"
style={{
position: 'fixed',
left: rect.left + rect.width / 2,
top: rect.bottom + 5
}}
>
{content}
</div>,
tooltipRoot
);
};
总结
处理 React Portal 中的 TypeScript 类型错误关键在于理解 getElementById()
可能返回 null 的特性。通过类型断言、null 检查或条件渲染,我们可以安全地使用 Portal 功能,同时保持类型安全。
选择哪种解决方案取决于你的具体场景:
- 确定元素存在时:使用类型断言 (
as HTMLElement
) - 需要错误处理时:添加显式 null 检查
- 元素可能不存在时:使用条件渲染
始终优先考虑类型安全和代码的健壮性,避免禁用 TypeScript 的重要检查功能。