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 层级渲染的交互上,而不只是检查按钮、表格和表单等静态内容。
优先覆盖以下路径:
- Modal 连续打开、关闭,再次打开;
- Tooltip 和 Popover 快速显示与隐藏;
- Select、Cascader 等选择组件的下拉层;
- 组件卸载时仍处于打开状态的浮层;
- 路由切换后是否遗留 Portal 容器;
- 自定义
getPopupContainer或挂载目标的页面; - 开发环境启用
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 之上的关键交互。