diff --git a/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx index e98acc44e61e7..01a63f6171d93 100644 --- a/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx @@ -10,17 +10,21 @@ import { css } from '@emotion/react'; import { PresentationPanel } from '@kbn/presentation-panel-plugin/public'; import { PanelCompatibleComponent } from '@kbn/presentation-panel-plugin/public/panel_component/types'; import { isPromise } from '@kbn/std'; -import React, { ReactNode, useEffect, useImperativeHandle, useMemo, useState } from 'react'; +import React, { ReactNode, useEffect, useImperativeHandle, useMemo, useState, useRef } from 'react'; import { untilPluginStartServicesReady } from '../kibana_services'; import { EmbeddablePanelProps } from './types'; const getComponentFromEmbeddable = async ( - embeddable: EmbeddablePanelProps['embeddable'] -): Promise => { + embeddable: EmbeddablePanelProps['embeddable'], + isMounted: () => boolean +): Promise => { const startServicesPromise = untilPluginStartServicesReady(); const embeddablePromise = typeof embeddable === 'function' ? embeddable() : Promise.resolve(embeddable); const [, unwrappedEmbeddable] = await Promise.all([startServicesPromise, embeddablePromise]); + if (!isMounted()) { + return null; + } if (unwrappedEmbeddable.parent) { await unwrappedEmbeddable.parent.untilEmbeddableLoaded(unwrappedEmbeddable.id); } @@ -55,9 +59,33 @@ const getComponentFromEmbeddable = async ( /** * Loads and renders a legacy embeddable. + * + * Ancestry chain must use 'key' attribute to reset DOM and state when embeddable changes + * For example */ export const EmbeddablePanel = (props: EmbeddablePanelProps) => { + // can not use useMountedState + // 1. useMountedState defaults mountedRef to false and sets mountedRef to true in useEffect + // 2. embeddable can be an object or a function that returns a promise + // 3. when embeddable is an object, Promise.resolve(embeddable) returns before + // useMountedState useEffect is called and thus isMounted() returns false when component has not been unmounted + const mountedRef = useRef(true); + useEffect(() => { + return () => { + mountedRef.current = false; + }; + }, []); + const isMounted = () => { + return mountedRef.current; + }; const { embeddable, ...passThroughProps } = props; - const componentPromise = useMemo(() => getComponentFromEmbeddable(embeddable), [embeddable]); + const componentPromise = useMemo( + () => getComponentFromEmbeddable(embeddable, isMounted), + // Ancestry chain is expected to use 'key' attribute to reset DOM and state + // when embeddable needs to be re-loaded + // empty array is consistent with PresentationPanel useAsync dependency check + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); return ; }; diff --git a/src/plugins/presentation_panel/public/panel_component/presentation_panel.tsx b/src/plugins/presentation_panel/public/panel_component/presentation_panel.tsx index 561fefe8d1ec6..afc7714ea022c 100644 --- a/src/plugins/presentation_panel/public/panel_component/presentation_panel.tsx +++ b/src/plugins/presentation_panel/public/panel_component/presentation_panel.tsx @@ -36,6 +36,8 @@ export const PresentationPanel = < ]); const Panel = panelModule.PresentationPanelInternal; return { Panel, unwrappedComponent }; + // Ancestry chain is expected to use 'key' attribute to reset DOM and state + // when unwrappedComponent needs to be re-loaded }, []); if (error || (!loading && (!value?.Panel || !value?.unwrappedComponent))) { diff --git a/src/plugins/presentation_panel/public/panel_component/types.ts b/src/plugins/presentation_panel/public/panel_component/types.ts index 654626e5959d6..aaee39d43939a 100644 --- a/src/plugins/presentation_panel/public/panel_component/types.ts +++ b/src/plugins/presentation_panel/public/panel_component/types.ts @@ -77,5 +77,5 @@ export type PresentationPanelProps< ApiType extends DefaultPresentationPanelApi = DefaultPresentationPanelApi, PropsType extends {} = {} > = Omit, 'Component'> & { - Component: MaybePromise>; + Component: MaybePromise | null>; };