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

React Testing Library测试Portal组件元素不存在解决方案

React Portal组件测试失败解决方案

问题场景

现有一个通过React createPortal 实现的弹窗组件,会将内容挂载到id为#___portal的全局常驻div节点下,节点基础结构为:

<div id="___portal"></div>

组件实现代码:

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

const App = ({ children }) => {
  const [portalEl, setPortalEl] = useState();

  useEffect(() => {
    // 实际业务环境中 #___portal 节点始终存在
    setPortalEl(document.getElementById("___portal"));
    console.log(portalEl);
  }, [portalEl]);

  if (!portalEl) return null;

  return createPortal(
    <>
      <div id="modal" role="dialog">
        {children}
      </div>
    </>,
    portalEl
  );
};

export default App;

编写测试用例验证组件是否能正常渲染传入的文本内容时,测试失败,打印的DOM结构中#___portal节点为空,内容未成功挂载。
测试用例代码:

import App from "./App";
import React from "react";
import { render } from "react-testing-library";
describe("<App />", () => {
  it("Should render text", async () => {
    let testId = "___portal";
    let newDiv = document.createElement("div");
    newDiv.setAttribute("id", testId);
    document.body.appendChild(newDiv);

    const text = "hello world";
    const { getByText } = render(
      <App>
        <div>{text}</div>
      </App>
    );
    expect(getByText(text)).toBeInTheDocument();
  });
});

测试执行时输出的DOM结构:

<body>
 <div id="___portal" />
 <div />
</body>

失败原因

  • 组件中获取portal节点的逻辑放在useEffect里,首次渲染时portalEl为undefined,组件直接返回null,等effect执行完赋值后才会触发二次渲染挂载portal内容,但测试用例没有等待异步状态更新完成就直接执行断言。
  • 旧版react-testing-library的render返回的查询方法默认只会搜索React挂载根节点下的内容,portal是挂载到根节点外的#___portal节点,不在默认查询范围内。
  • useEffect的依赖数组包含portalEl,会导致effect在portalEl更新后重复触发,存在冗余执行。

修复方案

1. 优化组件逻辑

去掉不必要的effect依赖,直接在state初始化时获取portal节点,避免首次渲染返回null导致的额外重渲染:

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

const App = ({ children }) => {
  // 初始化时直接获取portal节点,不需要等待useEffect执行
  const [portalEl] = useState(() => document.getElementById("___portal"));

  if (!portalEl) return null;

  return createPortal(
    <div id="modal" role="dialog">
      {children}
    </div>,
    portalEl
  );
};

export default App;

2. 调整测试用例

  • 每个用例执行前清理DOM,避免全局节点污染
  • 优先使用screen全局查询方法,它默认搜索整个document范围,不受React挂载容器限制,适配portal场景
  • 如果保留原有effect逻辑,需要用waitFor等待异步渲染完成后再断言
    调整后的测试代码:
import App from "./App";
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";

describe("<App />", () => {
  beforeEach(() => {
    // 测试前清空body,避免重复添加节点
    document.body.innerHTML = "";
  });

  it("Should render text", async () => {
    // 提前创建portal挂载节点
    const portalContainer = document.createElement("div");
    portalContainer.id = "___portal";
    document.body.appendChild(portalContainer);

    const testText = "hello world";
    render(
      <App>
        <div>{testText}</div>
      </App>
    );

    // 等待内容挂载完成后断言
    await waitFor(() => {
      expect(screen.getByText(testText)).toBeInTheDocument();
    });
  });
});

提示:如果使用的是支持portal配置的Testing Library版本,也可以在render时直接指定portal容器,不需要手动创建节点。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.02 04:06:31