diff --git a/packages/devextreme-react/src/core/component-base.tsx b/packages/devextreme-react/src/core/component-base.tsx index 3df41914dff9..4fbb8aa80baa 100644 --- a/packages/devextreme-react/src/core/component-base.tsx +++ b/packages/devextreme-react/src/core/component-base.tsx @@ -49,7 +49,7 @@ type ComponentBaseProps = ComponentProps & { interface ComponentBaseRef { getInstance: () => any; getElement: () => HTMLDivElement | undefined; - createWidget: (element?: Element) => void; + createWidget: (element?: Element) => boolean; } interface IHtmlOptions { @@ -155,6 +155,14 @@ const ComponentBase = forwardRef( } }, []); + const getInstanceHost = (inst: any): Element | undefined => { + try { + return inst?.element?.(); + } catch { + return undefined; + } + }; + const setInlineStyles = useCallback((styles) => { if (element.current) { const el = element.current; @@ -217,12 +225,18 @@ const ComponentBase = forwardRef( unscheduleGuards(); }, []); - const createWidget = useCallback((el?: Element) => { - beforeCreateWidget(); - + const createWidget = useCallback((el?: Element): boolean => { // eslint-disable-next-line no-param-reassign el = el || element.current; + const currentHost = getInstanceHost(instance.current); + if (instance.current && el && currentHost === el) { + return false; // reuse existing instance on same host (Activity resume) + } + + // Only run these hooks when we actually create + beforeCreateWidget(); + let options: any = { templatesRenderAsynchronously: true, ...optionsManager.current.getInitialOptions(widgetConfig), @@ -255,6 +269,7 @@ const ComponentBase = forwardRef( instance.current.on('optionChanged', optionsManager.current.onOptionChanged); afterCreateWidget(); + return true; }, [ beforeCreateWidget, afterCreateWidget, @@ -294,6 +309,18 @@ const ComponentBase = forwardRef( ]); const onComponentMounted = useCallback(() => { + // Activity resume: instance exists but effects were torn down -> refresh layout after showing + if (instance.current) { + requestAnimationFrame(() => { + const inst = instance.current; + if (inst?.updateDimensions) { + inst.updateDimensions(); + } else if (inst?.repaint) { + inst.repaint(); + } + }); + } + const { style } = props; if (childElementsDetached.current) { @@ -309,12 +336,13 @@ const ComponentBase = forwardRef( prevPropsRef.current = props; }, [ + restoreTree, updateCssClasses, setInlineStyles, props, ]); - const onComponentUnmounted = useCallback(() => { + const disposeWidgetNow = useCallback(() => { removalLocker?.lock(); if (instance.current) { @@ -340,6 +368,25 @@ const ComponentBase = forwardRef( removalLocker?.unlock(); }, [removalLocker]); + const onComponentUnmounted = useCallback(() => { + const el = element.current; + const disposedRef = { disposed: false }; + + const checkAndDispose = () => { + if (disposedRef.disposed) return; + + // If element is still connected, it is likely Activity hide -> keep instance + if (el?.isConnected) return; + + disposedRef.disposed = true; + disposeWidgetNow(); + }; + + // Run in both microtask and macrotask to avoid timing edge cases + queueMicrotask(checkAndDispose); + setTimeout(checkAndDispose, 0); + }, [disposeWidgetNow]); + useLayoutEffect(() => { onComponentMounted(); @@ -371,7 +418,7 @@ const ComponentBase = forwardRef( return element.current; }, createWidget(el) { - createWidgetRef.current?.(el); + return createWidgetRef.current?.(el) ?? false; }, } ), []); diff --git a/packages/devextreme-react/src/core/component.tsx b/packages/devextreme-react/src/core/component.tsx index eb6d5ddcccb0..b503688c6ce2 100644 --- a/packages/devextreme-react/src/core/component.tsx +++ b/packages/devextreme-react/src/core/component.tsx @@ -63,9 +63,9 @@ const Component = forwardRef( }, ), [props, registerExtension]); - const createWidget = useCallback((el?: Element) => { - componentBaseRef.current?.createWidget(el); - }, []); + const createWidget = useCallback( + (el?: Element) => componentBaseRef.current?.createWidget(el) ?? false, [] + ); const clearExtensions = useCallback(() => { props.clearExtensions?.(); @@ -76,11 +76,27 @@ const Component = forwardRef( const clearExtensionsRef = useRef(clearExtensions); useLayoutEffect(() => { - createWidget(); - createExtensions(); + const created = createWidget(); + if (created) { + createExtensions(); + } return () => { - clearExtensionsRef.current?.(); + const el = componentBaseRef.current?.getElement(); + const clearedRef = { cleared: false }; + + const checkAndClear = () => { + if (clearedRef.cleared) return; + + // If still connected, it's likely Activity hide -> do nothing + if (el?.isConnected) return; + + clearedRef.cleared = true; + clearExtensionsRef.current?.(); + }; + + queueMicrotask(checkAndClear); + setTimeout(checkAndClear, 0); }; }, []); @@ -101,7 +117,7 @@ const Component = forwardRef( return componentBaseRef.current?.getElement(); }, createWidget(el) { - createWidgetRef.current?.(el); + return createWidgetRef.current?.(el) ?? false; }, clearExtensions() { clearExtensionsRef.current?.(); diff --git a/packages/devextreme-react/src/core/extension-component.tsx b/packages/devextreme-react/src/core/extension-component.tsx index d20b03dcbd47..cc06d326e732 100644 --- a/packages/devextreme-react/src/core/extension-component.tsx +++ b/packages/devextreme-react/src/core/extension-component.tsx @@ -25,9 +25,9 @@ const ExtensionComponent = forwardRef(

(props: P & ComponentProps, ref: React.ForwardedRef) => { const componentBaseRef = useRef(null); - const createWidget = useCallback((el?: Element) => { - componentBaseRef.current?.createWidget(el); - }, []); + const createWidget = useCallback( + (el?: Element) => componentBaseRef.current?.createWidget(el) ?? false, [] + ); useLayoutEffect(() => { const { onMounted } = props as any; @@ -53,7 +53,7 @@ const ExtensionComponent = forwardRef( return componentBaseRef.current?.getElement(); }, createWidget(el) { - createWidgetRef.current?.(el); + return createWidgetRef.current?.(el) ?? false; }, } ), []);