Skip to content

处理 React Portal 中的 TypeScript 类型错误

问题描述

在使用 React Portal 时,经常会遇到 TypeScript 类型错误:

typescript
Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Element'.
Type 'null' is not assignable to type 'Element'.ts(2345)

这个错误通常出现在类似下面的代码中:

html
<body>
    <div id="portal"></div>
    <div id="root"></div>
</body>
typescript
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 值,从而防止运行时错误。

解决方案

方法一:使用类型断言(推荐)

如果你确定元素一定存在,可以使用类型断言:

typescript
const portalDiv = document.getElementById('portal') as HTMLElement;

或者使用非空断言操作符:

typescript
const portalDiv = document.getElementById('portal')!;

注意事项

  • 只在你能 100% 确定元素存在时使用这种方法
  • 如果元素可能不存在,应该添加适当的错误处理

方法二:添加 null 检查

更安全的方法是在使用前检查元素是否存在:

typescript
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
  );
}

方法三:条件渲染

如果元素可能不存在,可以在组件内部进行条件渲染:

typescript
function Portal1({ children }) {
  const portalDiv = document.getElementById('portal');
  
  return portalDiv 
    ? ReactDOM.createPortal(<>{children}</>, portalDiv) 
    : null;
}

方法四:React Hooks 中的处理

在函数组件中使用 useEffect 处理 Portal:

typescript
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;

最佳实践建议

  1. 启用严格模式:不要禁用 strictNullChecks,这是 TypeScript 的重要安全特性

  2. 防御性编程:总是假设 DOM 元素可能不存在,特别是在组件挂载的早期阶段

  3. 错误边界:对于关键的 Portal 容器,添加适当的错误处理

  4. 使用 React Refs:对于动态内容,考虑使用 React ref 而不是直接 DOM 查询

常见使用场景

typescript
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
  );
};
typescript
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 的重要检查功能。