From 67d6fc621c077b7acbfc1f2ed11d3bae3c672f81 Mon Sep 17 00:00:00 2001 From: lijinke666 Date: Wed, 3 Jul 2024 14:09:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=90=8C=E6=97=B6?= =?UTF-8?q?=E5=9C=A8=20React=2016/17/18=20=E4=B8=8B=E8=BF=90=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../base-interaction/click/data-cell-click.ts | 1 + packages/s2-react/playground/index.tsx | 4 +- .../custom-cell/edit-cell/index.tsx | 3 +- .../src/components/tooltip/custom-tooltip.tsx | 39 +----- .../s2-react/src/utils/invokeComponent.tsx | 37 +++-- packages/s2-react/src/utils/reactRender.ts | 130 ++++++++++++++++++ 6 files changed, 157 insertions(+), 57 deletions(-) create mode 100644 packages/s2-react/src/utils/reactRender.ts diff --git a/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts b/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts index afc117b292..77f24ac4a2 100644 --- a/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts +++ b/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts @@ -122,6 +122,7 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement { operator, hideSummary: true, onlyShowCellText, + forceRender: true, }); } diff --git a/packages/s2-react/playground/index.tsx b/packages/s2-react/playground/index.tsx index aa95421179..3cfb054eb5 100644 --- a/packages/s2-react/playground/index.tsx +++ b/packages/s2-react/playground/index.tsx @@ -45,11 +45,11 @@ import { import { debounce, isEmpty } from 'lodash'; import React from 'react'; import { ChromePicker } from 'react-color'; -import { createRoot } from 'react-dom/client'; import reactPkg from '../package.json'; import type { SheetComponentOptions } from '../src'; import { SheetComponent } from '../src'; import { ConfigProvider } from '../src/components/config-provider'; +import { reactRender } from '../src/utils/reactRender'; import { BigDataSheet } from './components/BigDataSheet'; import { ChartSheet } from './components/ChartSheet'; import { CustomGrid } from './components/CustomGrid'; @@ -1648,4 +1648,4 @@ function MainLayout() { ); } -createRoot(document.getElementById('root')!).render(); +reactRender(, document.getElementById('root')!); diff --git a/packages/s2-react/src/components/sheets/editable-sheet/custom-cell/edit-cell/index.tsx b/packages/s2-react/src/components/sheets/editable-sheet/custom-cell/edit-cell/index.tsx index 56e86c872c..860c825b22 100644 --- a/packages/s2-react/src/components/sheets/editable-sheet/custom-cell/edit-cell/index.tsx +++ b/packages/s2-react/src/components/sheets/editable-sheet/custom-cell/edit-cell/index.tsx @@ -1,6 +1,7 @@ import { GEvent, S2Event, + S2_PREFIX_CLS, SpreadSheet, TableDataCell, customMerge, @@ -179,7 +180,7 @@ function EditCellComponent( { - root: Root | null; - - isLegacyReactVersion = !startsWith(version, '18'); - constructor(spreadsheet: SpreadSheet) { super(spreadsheet); } @@ -61,14 +54,7 @@ export class CustomTooltip extends BaseTooltip< ); - if (this.isLegacyReactVersion) { - render(TooltipContent, this.container); - - return; - } - - this.root ??= createRoot(this.container!); - this.root.render(TooltipContent); + reactRender(TooltipContent, this.container!); } hide() { @@ -84,27 +70,10 @@ export class CustomTooltip extends BaseTooltip< } forceClearContent() { - if (this.isLegacyReactVersion) { - this.unmount(); - - return; - } - - this.root?.render(null); + forceClear(this.container!); } unmount() { - if (this.isLegacyReactVersion && this.container!) { - unmountComponentAtNode(this.container); - - return; - } - - // https://github.com/facebook/react/issues/25675#issuecomment-1363957941 - Promise.resolve().then(() => { - this.root?.unmount(); - // Fiber 节点卸载后不能再重新渲染, 需要重新创建 - this.root = null; - }); + reactUnmount(this.container!); } } diff --git a/packages/s2-react/src/utils/invokeComponent.tsx b/packages/s2-react/src/utils/invokeComponent.tsx index 776c5f9854..5c2072e99a 100644 --- a/packages/s2-react/src/utils/invokeComponent.tsx +++ b/packages/s2-react/src/utils/invokeComponent.tsx @@ -1,11 +1,11 @@ import type { SpreadSheet } from '@antv/s2'; import React from 'react'; -import { createRoot, type Root } from 'react-dom/client'; import { SpreadSheetContext } from '../context/SpreadSheetContext'; +import { reactRender, reactUnmount } from './reactRender'; export type InvokeComponentProps

= { - onCancel: any; - resolver: (val: boolean) => any; + onCancel: () => void; + resolver: (val: boolean) => void; params: P; }; @@ -24,20 +24,19 @@ export function invokeComponent

(options: InvokeComponentOptions

) { const { id, s2, params, onCleanup, component: Component } = options; if (id) { - const domNode = document.querySelector(`#${id}`); + const container = document.querySelector(`#${id}`); - if (domNode) { - // const unmountResult = ReactDOM.unmountComponentAtNode(domNode); + if (container) { + const result = reactUnmount(container); - if (domNode.parentNode) { - domNode.parentNode.removeChild(domNode); + if (result && container.parentNode) { + container.parentNode.removeChild(container); return; } } } - let root: Root; const container = document.createElement('div'); if (id) { @@ -46,11 +45,12 @@ export function invokeComponent

(options: InvokeComponentOptions

) { document.body.appendChild(container); - let resolveCb: (value: unknown) => void; + let resolveCb: (value: boolean) => void; let rejectCb: (reason?: unknown) => void; function destroy() { - root?.unmount(); + reactUnmount(container); + if (container.parentNode) { container.parentNode.removeChild(container); @@ -60,33 +60,32 @@ export function invokeComponent

(options: InvokeComponentOptions

) { } } - function close() { + function onClose() { destroy(); rejectCb(); } - const prom = new Promise((resolve, reject) => { + const handler = new Promise((resolve, reject) => { resolveCb = resolve; rejectCb = reject; }).then((val) => { - close(); + onClose(); return val; }); function render() { setTimeout(() => { - root = createRoot(container!); - - root.render( + reactRender( - + , + container, ); }); } render(); - return prom; + return handler; } diff --git a/packages/s2-react/src/utils/reactRender.ts b/packages/s2-react/src/utils/reactRender.ts new file mode 100644 index 0000000000..522e4ce2ce --- /dev/null +++ b/packages/s2-react/src/utils/reactRender.ts @@ -0,0 +1,130 @@ +/** + * 参考 react-component/util 同时兼容 React 16/17/18 的挂载和卸载 + * @link https://github.com/react-component/util/blob/677d3ac177d147572b65af63e67a7796a5104f4c/src/React/render.ts + */ +import { version } from 'react'; +import * as ReactDOM from 'react-dom'; +import type { Root } from 'react-dom/client'; + +const REACT_ROOT_SYMBOL_ID = `__s2_react_root__`; + +type ContainerType = (Element | DocumentFragment) & { + [REACT_ROOT_SYMBOL_ID]?: Root; +}; + +type CreateRoot = (container: ContainerType) => Root; + +const ReactDOMClone = { + ...ReactDOM, +} as typeof ReactDOM & { + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?: { + usingClientEntryPoint?: boolean; + }; + createRoot?: CreateRoot; +}; + +const { render: reactOriginalRender, unmountComponentAtNode } = ReactDOMClone; + +let createRoot: CreateRoot; + +try { + const mainVersion = Number((version || '').split('.')[0]); + + if (mainVersion >= 18) { + createRoot = ReactDOMClone.createRoot!; + } +} catch (e) { + // < React 18 +} + +export const isLegacyReactVersion = () => { + return !createRoot; +}; + +/** + * 由于兼容的关系, 没有从 "react-dom/client" 引入 createRoot, 会报 warning + * https://github.com/facebook/react/issues/24372 + */ +function toggleWarning(skip: boolean) { + const { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } = ReactDOMClone; + + if ( + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED && + typeof __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED === 'object' + ) { + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = + skip; + } +} + +// ========================== Render ========================== + +function modernRender( + node: React.ReactElement | null, + container: ContainerType, +): Root { + toggleWarning(true); + const root = container[REACT_ROOT_SYMBOL_ID] || createRoot(container); + + toggleWarning(false); + + root.render(node); + + container[REACT_ROOT_SYMBOL_ID] = root; + + return root; +} + +function legacyRender( + node: React.ReactElement | null, + container: ContainerType, +) { + reactOriginalRender(node!, container); +} + +export function reactRender( + node: React.ReactElement | null, + container: ContainerType, +) { + if (!isLegacyReactVersion()) { + modernRender(node, container); + + return; + } + + legacyRender(node, container); +} + +// ========================= Unmount ========================== +function modernUnmount(container: ContainerType) { + // https://github.com/facebook/react/issues/25675#issuecomment-1363957941 + return Promise.resolve().then(() => { + container?.[REACT_ROOT_SYMBOL_ID]?.unmount(); + + delete container?.[REACT_ROOT_SYMBOL_ID]; + }); +} + +function legacyUnmount(container: ContainerType) { + if (container) { + unmountComponentAtNode(container); + } +} + +export function reactUnmount(container: ContainerType) { + if (!isLegacyReactVersion()) { + return modernUnmount(container); + } + + return legacyUnmount(container); +} + +export function forceClear(container: ContainerType) { + if (isLegacyReactVersion()) { + legacyUnmount(container); + + return; + } + + modernRender(null, container); +}