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&2:ref类型与只读属性错误
- 保留
Element | DocumentFragment | null作为ref的类型是正确的:因为useRef初始值为null,且document.querySelector(selector)可能返回null(找不到匹配元素时)。如果移除类型中的null,TS会因为初始值null与类型不匹配,将current判定为只读属性,导致无法赋值。
- 保留
问题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
相关产品推荐
相关产品推荐

