diff --git a/change/@fluentui-react-positioning-4bd82184-0e7b-46e8-a681-c87900b25568.json b/change/@fluentui-react-positioning-4bd82184-0e7b-46e8-a681-c87900b25568.json new file mode 100644 index 0000000000000..5631d40cf1cde --- /dev/null +++ b/change/@fluentui-react-positioning-4bd82184-0e7b-46e8-a681-c87900b25568.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix(createPositionManager): computePosition should not apply styles after destruction", + "packageName": "@fluentui/react-positioning", + "email": "lingfangao@hotmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-positioning/src/createPositionManager.ts b/packages/react-components/react-positioning/src/createPositionManager.ts index 1dcde740ee90e..1671c0ada2fd0 100644 --- a/packages/react-components/react-positioning/src/createPositionManager.ts +++ b/packages/react-components/react-positioning/src/createPositionManager.ts @@ -37,6 +37,7 @@ interface PositionManagerOptions { */ export function createPositionManager(options: PositionManagerOptions): PositionManager { const { container, target, arrow, strategy, middleware, placement } = options; + let isDestroyed = false; if (!target || !container) { return { updatePosition: () => undefined, @@ -52,7 +53,13 @@ export function createPositionManager(options: PositionManagerOptions): Position // Without this scroll jumps can occur when the element is rendered initially and receives focus Object.assign(container.style, { position: 'fixed', left: 0, top: 0, margin: 0 }); - let forceUpdate = () => { + const forceUpdate = () => { + // debounced update can still occur afterwards + // early return to avoid memory leaks + if (isDestroyed) { + return; + } + if (isFirstUpdate) { scrollParents.add(getScrollParent(container)); if (target instanceof HTMLElement) { @@ -69,6 +76,12 @@ export function createPositionManager(options: PositionManagerOptions): Position Object.assign(container.style, { position: strategy }); computePosition(target, container, { placement, middleware, strategy }) .then(({ x, y, middlewareData, placement: computedPlacement }) => { + // Promise can still resolve after destruction + // early return to avoid applying outdated position + if (isDestroyed) { + return; + } + writeArrowUpdates({ arrow, middlewareData }); writeContainerUpdates({ container, @@ -97,9 +110,7 @@ export function createPositionManager(options: PositionManagerOptions): Position const updatePosition = debounce(() => forceUpdate()); const dispose = () => { - // debounced update can still occur afterwards - // so destroy the reference to forceUpdate - forceUpdate = () => null; + isDestroyed = true; if (targetWindow) { targetWindow.removeEventListener('scroll', updatePosition);