Semi Design v2.101.1:修复 React 18 StrictMode 下的 Portal 稳定性问题

2026-07-20 19 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

Semi Design v2.101.1 已发布。本次更新的重点不是新增一批可见组件,而是修复 Portal 在 React 18 及以上版本的 StrictMode 模拟重新挂载场景中的问题。对于大量使用 Modal、Tooltip、Dropdown 等浮层组件的中后台应用,这类底层修复会直接影响开发环境中的稳定性,也值得升级后进行针对性回归。

为什么 StrictMode 会暴露 Portal 问题

Semi Design 是由字节跳动抖音前端与 UED 团队设计、开发并维护的企业级设计系统,提供 React 组件、设计语言与主题能力。它覆盖的弹窗、提示、选择器和下拉菜单等组件,通常需要通过 Portal 把浮层渲染到页面主体或指定容器中。

React 18 的 StrictMode 会在开发环境中模拟组件的重新挂载过程,用来发现副作用清理不完整、生命周期逻辑不可重复执行等问题。它不会简单地把所有逻辑“多执行一次”,而是在刻意检验组件能否正确经历创建、清理和再次创建。

Portal 实现尤其容易受到影响,因为它往往涉及:

  • 动态创建并挂载 DOM 容器;
  • 在组件卸载时移除节点和事件监听器;
  • 保存目标容器引用;
  • 管理浮层动画、焦点与滚动锁定;
  • 在重新挂载时恢复正确的父子关系。

如果清理逻辑和初始化逻辑不对称,开发环境中可能出现浮层不显示、重复节点、容器引用失效或控制台异常。来源摘要只明确指出 v2.101.1 修复了 Portal 在 React 18+ StrictMode 模拟重新挂载时的问题,因此具体受影响组件和异常表现仍应以项目中的实际回归结果为准。

升级时不要只检查页面能否启动

Portal 是多个上层组件共享的基础设施。升级后,建议把测试重点放在会脱离当前 DOM 层级渲染的交互上,而不只是检查按钮、表格和表单等静态内容。

优先覆盖以下路径:

  1. Modal 连续打开、关闭,再次打开;
  2. Tooltip 和 Popover 快速显示与隐藏;
  3. Select、Cascader 等选择组件的下拉层;
  4. 组件卸载时仍处于打开状态的浮层;
  5. 路由切换后是否遗留 Portal 容器;
  6. 自定义 getPopupContainer 或挂载目标的页面;
  7. 开发环境启用 StrictMode 后的控制台错误与警告。

还要注意,StrictMode 的模拟行为主要用于开发检查。生产环境看似正常,并不能证明副作用管理没有缺陷;反过来,只在开发环境复现的问题也不应直接归因于 React 本身。

可以这样实践:搭建一个最小回归项目

下面的示例不是来源提供的官方复现代码,而是一套可以直接改造的验证工程。它固定使用 Semi Design 2.101.1,并通过 React 18 的 StrictMode 反复挂载包含 Modal 和 Tooltip 的组件。

先创建项目并安装依赖:

npm create vite@latest semi-portal-check -- --template react
cd semi-portal-check
npm install
npm install @douyinfe/semi-ui@2.101.1
npm run dev

src/main.jsx 替换为:

import React, { StrictMode, useState } from 'react';
import ReactDOM from 'react-dom/client';
import { Button, Modal, Space, Tooltip } from '@douyinfe/semi-ui';
import '@douyinfe/semi-ui/dist/css/semi.min.css';
import './index.css';

function PortalChecks() {
  const [visible, setVisible] = useState(false);

  return (
    <Space vertical align="start">
      <Tooltip content="这个提示通过浮层呈现">
        <Button>悬停检查 Tooltip</Button>
      </Tooltip>

      <Button theme="solid" onClick={() => setVisible(true)}>
        打开 Modal
      </Button>

      <Modal
        title="Portal 回归检查"
        visible={visible}
        onCancel={() => setVisible(false)}
        onOk={() => setVisible(false)}
      >
        连续打开关闭弹窗并观察控制台和 DOM 节点
      </Modal>
    </Space>
  );
}

function App() {
  const [mounted, setMounted] = useState(true);

  return (
    <main>
      <h1>Semi Design Portal Check</h1>
      <Button onClick={() => setMounted((value) => !value)}>
        {mounted ? '卸载测试区域' : '重新挂载测试区域'}
      </Button>
      <section>{mounted && <PortalChecks />}</section>
    </main>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

再将 src/index.css 替换为:

body {
  margin: 0;
  font-family: Inter, system-ui, sans-serif;
  background: #f5f6f7;
  color: #1f2329;
}

main {
  max-width: 720px;
  margin: 48px auto;
  padding: 24px;
  background: #fff;
}

section {
  margin-top: 24px;
}

运行页面后,依次执行这些动作:打开并关闭 Modal 多次、悬停 Tooltip、在 Modal 打开时卸载测试区域,然后重新挂载。与此同时检查浏览器控制台,并在 Elements 面板中观察 body 下是否持续累积无用浮层容器。

如果现有项目使用 pnpm,可以直接锁定补丁版本:

pnpm add @douyinfe/semi-ui@2.101.1 --save-exact
pnpm why @douyinfe/semi-ui react react-dom

第二条命令用于确认实际安装的 Semi Design、React 和 ReactDOM 版本,避免工作区依赖提升或锁文件状态让测试结果产生偏差。

自动化测试应覆盖“打开、卸载、再挂载”

只做截图测试可能捕捉不到重复 DOM 节点或卸载阶段异常。可以这样实践:在现有 Playwright 测试中加入一次完整生命周期检查。下面假设测试页面就是前述示例,并且按钮文案没有修改。

import { test, expect } from '@playwright/test';

test('portal components survive remounting', async ({ page }) => {
  const errors: string[] = [];
  page.on('console', (message) => {
    if (message.type() === 'error') errors.push(message.text());
  });
  page.on('pageerror', (error) => errors.push(error.message));

  await page.goto('http://localhost:5173');
  await page.getByRole('button', { name: '打开 Modal' }).click();
  await expect(page.getByText('Portal 回归检查')).toBeVisible();

  await page.getByRole('button', { name: '卸载测试区域' }).click();
  await page.getByRole('button', { name: '重新挂载测试区域' }).click();
  await page.getByRole('button', { name: '打开 Modal' }).click();

  await expect(page.getByText('Portal 回归检查')).toBeVisible();
  expect(errors).toEqual([]);
});

实际项目中应把 URL、定位器和允许出现的已知日志调整为自己的环境。测试目标不是断言 Semi Design 的内部 DOM 结构,而是验证用户可见行为、组件重新挂载能力以及控制台是否出现未处理异常。

是否应该立即采用

如果项目已经运行在 React 18 或更高版本、启用了 StrictMode,并大量使用 Semi Design 浮层组件,v2.101.1 是一个值得优先评估的补丁版本。采用时可以遵循一份简短清单:

  • 在独立分支中精确锁定 @douyinfe/semi-ui@2.101.1
  • 保留 StrictMode,不要通过关闭检查来掩盖生命周期问题;
  • 回归 Modal、Tooltip、Popover、Dropdown 和选择类组件;
  • 检查自定义 Portal 容器以及路由切换场景;
  • 同时观察 UI、控制台日志和遗留 DOM 节点;
  • 通过后再更新锁文件并进入常规发布流程。

本次发布体现了一个常见但重要的工程事实:设计系统的可靠性不只取决于组件外观,也取决于它能否适应 React 生命周期语义的变化。补丁虽小,验证范围却应覆盖所有建立在 Portal 之上的关键交互。


相关推荐