@douyinfe/semi-ui
Version:
A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.
213 lines (194 loc) • 7.39 kB
text/typescript
import type * as React from 'react';
import * as ReactDOM from 'react-dom';
import semiGlobal from './semi-global';
const fullClone: Record<string, any> = { ...ReactDOM };
const legacyRender: any = fullClone.render;
const legacyUnmount: any = fullClone.unmountComponentAtNode;
const legacyFindDOMNode: any = fullClone.findDOMNode;
const { version } = ReactDOM;
const mainVersion = Number((version || '').split('.')[0]);
// React 版本兼容性检查
let hasWarnedVersionMismatch = false;
function checkVersionCompatibility() {
if (hasWarnedVersionMismatch) {
return;
}
// 如果是 React 19+ 但没有注入 createRoot,在首次使用时会通过 warnCreateRootNotFound 警告
// 如果是 React < 18 但用户注入了 createRoot,给出警告
if (mainVersion < 18 && typeof semiGlobal.config?.createRoot === 'function') {
hasWarnedVersionMismatch = true;
console.warn(
`[Semi UI] createRoot was injected but React version is ${version} (< 18). ` +
'This configuration is unusual and may cause unexpected behavior.'
);
}
}
function toggleWarning(skip: boolean) {
const internals = fullClone.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
?? fullClone.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
if (internals && typeof internals === 'object') {
internals.usingClientEntryPoint = skip;
}
}
/**
* Resolve `createRoot` with 3-level fallback:
* 1. semiGlobal.config.createRoot (user injection, required for React 19)
* 2. fullClone.createRoot (auto-discovery from react-dom default export, works in React 18)
* 3. undefined → triggers console.error guiding user to inject
*/
function resolveCreateRoot(): CreateRootFn | undefined {
if (typeof semiGlobal.config?.createRoot === 'function') {
return semiGlobal.config.createRoot as CreateRootFn;
}
if (typeof fullClone.createRoot === 'function') {
return fullClone.createRoot;
}
return undefined;
}
let hasWarnedCreateRoot = false;
function warnCreateRootNotFound() {
if (hasWarnedCreateRoot) {
return;
}
hasWarnedCreateRoot = true;
console.error(
'[Semi UI] createRoot is not available. ' +
'If you are using React 19, please inject createRoot before using Semi components. ' +
'For details, see: https://semi.design/zh-CN/ecosystem/react19\n' +
'[Semi UI] createRoot 不可用。' +
'如果您正在使用 React 19,请在使用 Semi 组件前注入 createRoot。' +
'详情请参阅:https://semi.design/zh-CN/ecosystem/react19'
);
}
// ========================== Render ==========================
const MARK = '__semi_react_root__';
type CreateRootFn = (container: Element | DocumentFragment) => {
render(children: React.ReactNode): void;
unmount(): void;
};
type ContainerType = (Element | DocumentFragment) & {
[MARK]?: ReturnType<CreateRootFn>;
};
export function render(node: React.ReactElement, container: ContainerType) {
checkVersionCompatibility();
const createRoot = resolveCreateRoot();
if (createRoot) {
toggleWarning(true);
const root = container[MARK] || createRoot(container);
toggleWarning(false);
root.render(node);
container[MARK] = root;
} else if (legacyRender) {
legacyRender(node, container);
} else {
warnCreateRootNotFound();
}
}
export function unmount(container: ContainerType) {
// 优先检查是否有 createRoot 创建的 root,而不是检查 createRoot 是否可用
// 这样可以正确处理:用 legacyRender 渲染后,用户又注入了 createRoot 的情况
if (container[MARK]) {
container[MARK].unmount();
delete container[MARK];
} else if (legacyUnmount) {
legacyUnmount(container);
}
// 如果既没有 root 也没有 legacyUnmount,可能是:
// 1. 容器从未被渲染过
// 2. 容器已经被卸载过了
// 这两种情况都不需要警告,静默处理即可
}
// ======================== findDOMNode ========================
/**
* React 19+ fallback for findDOMNode: traverse React Fiber tree downward
* from a class component instance to find the first DOM element.
*
* Uses React internal Fiber structure (_reactInternals). If React changes
* its internals in future versions, this will safely return null without
* throwing errors, falling back to the warning path in resolveDOM.
*/
function findDOMFromFiber(instance: any): Element | null {
try {
const fiber = instance?._reactInternals ?? instance?._reactInternalFiber;
if (!fiber || typeof fiber !== 'object') {
return null;
}
let node = fiber.child;
let iterations = 0;
const MAX_ITERATIONS = 50;
while (node && iterations < MAX_ITERATIONS) {
iterations++;
// HostComponent: stateNode is a DOM element
if (node.stateNode instanceof Element) {
return node.stateNode;
}
if (node.child) {
node = node.child;
continue;
}
while (node && !node.sibling) {
if (node === fiber) {
return null;
}
node = node.return;
}
if (node && node !== fiber) {
node = node.sibling;
} else {
break;
}
}
} catch (e) {
// Fiber structure may have changed in a future React version; fail silently
}
return null;
}
/**
* React 16/17/18: use ReactDOM.findDOMNode to resolve real DOM from component instance.
* React 19: findDOMNode is removed; traverse Fiber tree to find DOM node.
*
* 注意:findDOMNode 可能返回 Text 节点,但我们只返回 Element 类型以保证类型安全。
*/
export function resolveDOM(instance: any): Element | null {
if (!instance) {
return null;
}
// 已经是 Element,直接返回
if (instance instanceof Element) {
return instance;
}
// 尝试使用 findDOMNode (React 16/17/18)
if (legacyFindDOMNode) {
try {
const node = legacyFindDOMNode(instance as React.ReactInstance);
// findDOMNode 可能返回 Text 节点,我们只返回 Element
if (node instanceof Element) {
return node;
}
return null;
} catch (e) {
// findDOMNode 可能在某些情况下抛出错误(如 StrictMode 警告)
return null;
}
}
// React 19 fallback: traverse Fiber tree to find the first DOM element
return findDOMFromFiber(instance);
}
// ========================= getRef ===========================
/**
* React 16/17/18: ref is a top-level property on the element (element.ref).
* React 19: ref is moved into element.props.ref.
*
* 使用版本检测来确定 ref 的位置,避免在 React 18 中错误地读取 props.ref。
*/
export function getRef(element: any): React.Ref<any> | null {
if (!element) {
return null;
}
// React 19+: ref 在 props 中
if (mainVersion >= 19) {
return element.props?.ref ?? null;
}
// React 16/17/18: ref 在顶层
return element.ref ?? null;
}