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

在React组件外调用createPortal无法更新DOM的问题排查

React中使用createPortal在自定义Handler类中无法渲染组件的问题

我尝试在React应用中通过自定义Handler类调用createPortal渲染动态组件,但组件无法显示。改用createRoot手动渲染则能正常工作,想知道忽略了哪些关键要点?

包含业务逻辑的Handler.ts

export class Handler {
  private element: HTMLElement | null;

  constructor(selector: string) {
      this.element = document.getElementById(selector);
  }

  attachedEvent() {
      this.element?.addEventListener("mouseenter", () => {
          let cancel = setTimeout(() => {
              if (this.element != null)
                  this.attachUi(this.element)
          }, 1000)

          this.element?.addEventListener('mouseleave', () => {
              clearTimeout(cancel)
          })
      })
  }

  attachUi(domNode: HTMLElement) {
      createPortal(createElement(
          'h1',
          {className: 'greeting'},
          'Hello'
      ), domNode);
  }
}

使用Handler.ts的React组件Main.tsx

const handler = new Handler("test_comp");
export default function Main() {

  useEffect(() => {
      // @ts-ignore
      handler.useAddEventListeners();
  });
  return (
      <>
        <div id="test_comp">
          <p>Detect Mouse</p>
        </div>
      </>
  )
 }

替换后可正常工作的attachUi函数

attachUi(domNode: HTMLElement) {
    const root = createRoot(domNode);
    root.render(createElement(
        'h1',
        {className: 'greeting'},
        'Hello'
    ));
  }

问题核心原因

createPortal的本质是React组件树的一部分,它必须在React的渲染生命周期内被调用——也就是要在某个React组件的render(或函数组件的返回值)中返回,才能被React的渲染器识别并处理。

你的Handler类是脱离React组件上下文的普通类,直接调用createPortal时,这个Portal元素并没有被纳入React的组件树,React根本不知道它的存在,自然不会去渲染。

而createRoot是直接创建了一个独立的React根节点,手动启动了渲染流程,相当于在指定DOM节点上初始化了一个新的React应用,所以能正常渲染内容。

正确使用createPortal的方式

如果要在鼠标交互场景下用createPortal,需要把逻辑融入React组件的生命周期:

  • 在组件内部维护Portal的显示状态(比如一个布尔值)
  • 把事件监听逻辑移到组件的useEffect中
  • 当触发显示条件时,在组件的返回值中返回createPortal的结果

示例代码:

import { useState, useEffect, useRef, createPortal } from 'react';

export default function Main() {
  const [showPortal, setShowPortal] = useState(false);
  const portalRef = useRef<HTMLDivElement>(null);
  let timerId: NodeJS.Timeout | null = null;

  useEffect(() => {
    const element = portalRef.current;
    if (!element) return;

    const handleMouseEnter = () => {
      timerId = setTimeout(() => setShowPortal(true), 1000);
    };

    const handleMouseLeave = () => {
      if (timerId) clearTimeout(timerId);
      setShowPortal(false);
    };

    element.addEventListener('mouseenter', handleMouseEnter);
    element.addEventListener('mouseleave', handleMouseLeave);

    return () => {
      element.removeEventListener('mouseenter', handleMouseEnter);
      element.removeEventListener('mouseleave', handleMouseLeave);
      if (timerId) clearTimeout(timerId);
    };
  }, []);

  return (
    <>
      <div ref={portalRef}>
        <p>Detect Mouse</p>
      </div>
      {showPortal && portalRef.current && createPortal(
        <h1 className="greeting">Hello</h1>,
        portalRef.current
      )}
    </>
  );
}

这样createPortal是在React组件的渲染流程中返回的,React会正确处理它的渲染、更新和卸载。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 18:32:28