diff --git a/packages/react/src/components/IonOverlayManager.tsx b/packages/react/src/components/IonOverlayManager.tsx index 4000fdfaa1..61b9513d47 100644 --- a/packages/react/src/components/IonOverlayManager.tsx +++ b/packages/react/src/components/IonOverlayManager.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import { ReactComponentOrElement } from '../models'; @@ -25,12 +25,29 @@ export const IonOverlayManager: React.FC = ({ onAddOverlay, onRemoveOverlay, }) => { - const [overlays, setOverlays] = useState<{ + type OverlaysList = { [key: string]: { component: any; containerElement: HTMLDivElement; }; - }>({}); + }; + + /** + * Because of the way we're passing around the addOverlay and removeOverlay + * callbacks, by the time they finally get called, they use a stale reference + * to the state that only has the initial values. So if two overlays are opened + * at the same time, both using useIonModal or similar (such as through nesting), + * the second will erase the first from the overlays list. This causes the content + * of the first overlay to unmount. + * + * We wrap the state in useRef to ensure the two callbacks always use the most + * up-to-date version. + * + * Further reading: https://stackoverflow.com/a/56554056 + */ + const [overlays, setOverlays] = useState({}); + const overlaysRef = useRef({}); + overlaysRef.current = overlays; useEffect(() => { /* Setup the callbacks that get called from */ @@ -43,13 +60,13 @@ export const IonOverlayManager: React.FC = ({ component: ReactComponentOrElement, containerElement: HTMLDivElement ) => { - const newOverlays = { ...overlays }; + const newOverlays = { ...overlaysRef.current }; newOverlays[id] = { component, containerElement }; setOverlays(newOverlays); }; const removeOverlay = (id: string) => { - const newOverlays = { ...overlays }; + const newOverlays = { ...overlaysRef.current }; delete newOverlays[id]; setOverlays(newOverlays); };