diff --git a/packages/react/src/components/IonOverlayManager.tsx b/packages/react/src/components/IonOverlayManager.tsx index 60e7ef1f92..0c07ea7823 100644 --- a/packages/react/src/components/IonOverlayManager.tsx +++ b/packages/react/src/components/IonOverlayManager.tsx @@ -40,7 +40,6 @@ export const IonOverlayManager: React.FC = ({ onAddOverl */ const [overlays, setOverlays] = useState({}); const overlaysRef = useRef({}); - overlaysRef.current = overlays; useEffect(() => { /* Setup the callbacks that get called from */ @@ -51,12 +50,50 @@ export const IonOverlayManager: React.FC = ({ onAddOverl const addOverlay = (id: string, component: ReactComponentOrElement, containerElement: HTMLDivElement) => { const newOverlays = { ...overlaysRef.current }; newOverlays[id] = { component, containerElement }; + + /** + * In order for this function to use the latest data + * we need to update the ref synchronously. + * However, updating a ref does not cause a re-render + * which is why we still update the state. + * + * Note that updating the ref in the body + * of IonOverlayManager is not sufficient + * because that relies on overlaysRef being + * updated as part of React's render loop. State updates + * are batched, so updating the state twice in quick succession does + * not necessarily result in 2 separate render calls. + * This means if two modals are added one after the other, + * the first modal will not have been added to + * overlaysRef since React has not re-rendered yet. + * More info: https://react.dev/reference/react/useState#setstate-caveats + */ + overlaysRef.current = newOverlays; setOverlays(newOverlays); }; const removeOverlay = (id: string) => { const newOverlays = { ...overlaysRef.current }; delete newOverlays[id]; + + /** + * In order for this function to use the latest data + * we need to update the ref synchronously. + * However, updating a ref does not cause a re-render + * which is why we still update the state. + * + * Note that updating the ref in the body + * of IonOverlayManager is not sufficient + * because that relies on overlaysRef being + * updated as part of React's render loop. State updates + * are batched, so updating the state twice in quick succession does + * not necessarily result in 2 separate render calls. + * This means if two modals are added one after the other, + * the first modal will not have been added to + * overlaysRef since React has not re-rendered yet. + * More info: https://react.dev/reference/react/useState#setstate-caveats + */ + overlaysRef.current = newOverlays; setOverlays(newOverlays); }; diff --git a/packages/react/test/base/src/pages/overlay-hooks/ModalHook.tsx b/packages/react/test/base/src/pages/overlay-hooks/ModalHook.tsx index 9712f5a226..fe3dfd0c28 100644 --- a/packages/react/test/base/src/pages/overlay-hooks/ModalHook.tsx +++ b/packages/react/test/base/src/pages/overlay-hooks/ModalHook.tsx @@ -30,6 +30,8 @@ const Body: React.FC<{ onDismiss({ test: true }, 'close')}> Close + + ); @@ -83,6 +85,14 @@ const ModalHook: React.FC = () => { const [presentModalWithContext] = useIonModal(ModalWithContext); + const [presentSecondaryModal] = useIonModal(ModalSecondary); + const [presentRootModal, dismissRootModal] = useIonModal(Body, { + onDismiss: () => { + dismissRootModal(); + presentSecondaryModal(); + } + }); + return ( @@ -134,6 +144,16 @@ const ModalHook: React.FC = () => { Show Modal with Context + { + presentRootModal() + }} + id="show-root-modal" + > + Show Root Modal + +
Count: {count}
Dismissed with role: {dismissedRole}
Data: {dismissedData && JSON.stringify(dismissedData)}
@@ -143,6 +163,15 @@ const ModalHook: React.FC = () => { ); }; +const ModalSecondary: React.FC = () => { + return ( +
+

Secondary Modal

+

This text should be visible

+
+ ) +} + const MyContext = React.createContext({ value: 'default value', }); diff --git a/packages/react/test/base/tests/e2e/specs/overlay-hooks/useIonModal.cy.ts b/packages/react/test/base/tests/e2e/specs/overlay-hooks/useIonModal.cy.ts index 64e904061c..12835ee7c7 100644 --- a/packages/react/test/base/tests/e2e/specs/overlay-hooks/useIonModal.cy.ts +++ b/packages/react/test/base/tests/e2e/specs/overlay-hooks/useIonModal.cy.ts @@ -70,4 +70,16 @@ describe('useIonModal', () => { //verify context value is overriden value cy.get('div').contains('overriden value') }); + + it('should render nested modal when modals are added and removed at the same time', () => { + cy.get('#show-root-modal').click(); + + cy.get('ion-modal').should('have.length', 1); + + cy.get('ion-modal #show-secondary-modal').click(); + + cy.get('ion-modal').should('have.length', 1); + + cy.get('ion-modal').contains('This text should be visible'); + }); });