You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React createPortal TypeScript报错:null无法赋值给Element/DocumentFragment

React Portal转TSX的TypeScript类型错误解决方案

场景说明

作为TypeScript新手,将React JS组件转为TSX时,因ESLint配置禁止使用非空断言!,转换Portal组件(使用createPortal)时遇到类型错误,原代码及问题如下:

import { useRef, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import React from "react";
import { useCallback } from "react";

type ClientOnlyPortalProps = {
  children: React.ReactNode;
  // Is there a type for CSS id?
  selector: string;
};

export default React.memo(function ClientOnlyPortal({
  children,
  selector,
}: ClientOnlyPortalProps) {
  const ref = useRef<Element | DocumentFragment | null>(null);
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                          //1. 不确定此处类型是否正确
  const [mounted, setMounted] = useState(false);

  useEffect(
    useCallback(() => {
      ref.current = document.querySelector(selector);
      setMounted(true); 
       ^^^^
         //2. 若移除第1处的null,此处会出现TS错误
      console.log("ClientOnlyPortal useEffect fired");
    }, [selector])
  );

  return mounted ? createPortal(children, ref.current) : null;
                                          ^^^^^^^^^^^
                                     //3. 此处始终存在错误
});

问题分析与解决

  1. 问题1&2:ref类型与只读属性错误

    • 保留Element | DocumentFragment | null作为ref的类型是正确的:因为useRef初始值为null,且document.querySelector(selector)可能返回null(找不到匹配元素时)。如果移除类型中的null,TS会因为初始值null与类型不匹配,将current判定为只读属性,导致无法赋值。
  2. 问题3:createPortal参数类型不匹配

    • 虽然mounted设为true,但TS无法保证ref.current一定有值(比如选择器无效的情况)。因此需要在返回前增加类型守卫,确保传入createPortal的第二个参数是合法的Element | DocumentFragment类型。

修改后的完整代码

import { useRef, useEffect, useState, useCallback } from "react";
import { createPortal } from "react-dom";

type ClientOnlyPortalProps = {
  children: React.ReactNode;
  selector: string;
  // 若仅允许传入ID选择器,可改为模板字面量类型:
  // selector: `#${string}`;
};

export default React.memo(function ClientOnlyPortal({
  children,
  selector,
}: ClientOnlyPortalProps) {
  // 类型包含null,适配初始值和querySelector的返回值
  const ref = useRef<Element | DocumentFragment | null>(null);
  const [mounted, setMounted] = useState(false);

  useEffect(
    useCallback(() => {
      ref.current = document.querySelector(selector);
      setMounted(true);
      console.log("ClientOnlyPortal useEffect fired");
    }, [selector])
  );

  // 双重检查:确保组件已挂载且目标元素存在
  if (!mounted || !ref.current) {
    return null;
  }

  return createPortal(children, ref.current);
});

额外补充:CSS选择器类型

TS没有专门的CSS选择器类型,用string即可满足通用场景。如果需要限制为ID选择器(必须以#开头),可以使用模板字面量类型selector: #${string}``,这样TS会自动校验传入的选择器格式。

内容的提问来源于stack exchange,提问作者tdev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 00:30:42