Skip to content

Commit 7b551fd

Browse files
liamdebeasithetaPCaveryjohnston
authored
fix(react): overlay content is shown with hook (#28109)
Issue number: resolves #28102 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> When one modal is added and another modal is removed, the modal that is removed does not account for the newly added modal when updating the overlay context in React. As a result, the inner contents of the newly added modal is not mounted. We originally tried to fix this in #24553, but the fix was not complete. While storing the latest information in a React ref was correct, the way we updated the ref was done in a way such that data was still stale. In particular, the `overlaysRef` is updated whenever `IonOverlayManager` is re-rendered. State updates are batched, so updating the state twice in quick succession does not necessarily result in 2 separate renders. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - We need to make sure the ref is updated synchronously before any render so that `addOverlay` and `removeOverlay` always have access to the latest data. - Added a test ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Dev build: `7.3.3-dev.11693592339.18e000af` --------- Co-authored-by: Maria Hutt <[email protected]> Co-authored-by: Amanda Johnston <[email protected]>
1 parent 176585f commit 7b551fd

File tree

3 files changed

+79
-1
lines changed

3 files changed

+79
-1
lines changed

packages/react/src/components/IonOverlayManager.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ export const IonOverlayManager: React.FC<IonOverlayManagerProps> = ({ onAddOverl
4040
*/
4141
const [overlays, setOverlays] = useState<OverlaysList>({});
4242
const overlaysRef = useRef<OverlaysList>({});
43-
overlaysRef.current = overlays;
4443

4544
useEffect(() => {
4645
/* Setup the callbacks that get called from <IonApp /> */
@@ -51,12 +50,50 @@ export const IonOverlayManager: React.FC<IonOverlayManagerProps> = ({ onAddOverl
5150
const addOverlay = (id: string, component: ReactComponentOrElement, containerElement: HTMLDivElement) => {
5251
const newOverlays = { ...overlaysRef.current };
5352
newOverlays[id] = { component, containerElement };
53+
54+
/**
55+
* In order for this function to use the latest data
56+
* we need to update the ref synchronously.
57+
* However, updating a ref does not cause a re-render
58+
* which is why we still update the state.
59+
*
60+
* Note that updating the ref in the body
61+
* of IonOverlayManager is not sufficient
62+
* because that relies on overlaysRef being
63+
* updated as part of React's render loop. State updates
64+
* are batched, so updating the state twice in quick succession does
65+
* not necessarily result in 2 separate render calls.
66+
* This means if two modals are added one after the other,
67+
* the first modal will not have been added to
68+
* overlaysRef since React has not re-rendered yet.
69+
* More info: https://react.dev/reference/react/useState#setstate-caveats
70+
*/
71+
overlaysRef.current = newOverlays;
5472
setOverlays(newOverlays);
5573
};
5674

5775
const removeOverlay = (id: string) => {
5876
const newOverlays = { ...overlaysRef.current };
5977
delete newOverlays[id];
78+
79+
/**
80+
* In order for this function to use the latest data
81+
* we need to update the ref synchronously.
82+
* However, updating a ref does not cause a re-render
83+
* which is why we still update the state.
84+
*
85+
* Note that updating the ref in the body
86+
* of IonOverlayManager is not sufficient
87+
* because that relies on overlaysRef being
88+
* updated as part of React's render loop. State updates
89+
* are batched, so updating the state twice in quick succession does
90+
* not necessarily result in 2 separate render calls.
91+
* This means if two modals are added one after the other,
92+
* the first modal will not have been added to
93+
* overlaysRef since React has not re-rendered yet.
94+
* More info: https://react.dev/reference/react/useState#setstate-caveats
95+
*/
96+
overlaysRef.current = newOverlays;
6097
setOverlays(newOverlays);
6198
};
6299

packages/react/test/base/src/pages/overlay-hooks/ModalHook.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const Body: React.FC<{
3030
<IonButton expand="block" onClick={() => onDismiss({ test: true }, 'close')}>
3131
Close
3232
</IonButton>
33+
34+
<button onClick={onDismiss} id="show-secondary-modal">Show Secondary Modal</button>
3335
</IonContent>
3436
</IonPage>
3537
);
@@ -83,6 +85,14 @@ const ModalHook: React.FC = () => {
8385

8486
const [presentModalWithContext] = useIonModal(ModalWithContext);
8587

88+
const [presentSecondaryModal] = useIonModal(ModalSecondary);
89+
const [presentRootModal, dismissRootModal] = useIonModal(Body, {
90+
onDismiss: () => {
91+
dismissRootModal();
92+
presentSecondaryModal();
93+
}
94+
});
95+
8696
return (
8797
<MyContext.Provider value={{ value: 'overriden value' }}>
8898
<IonPage>
@@ -134,6 +144,16 @@ const ModalHook: React.FC = () => {
134144
Show Modal with Context
135145
</IonButton>
136146

147+
<IonButton
148+
expand="block"
149+
onClick={() => {
150+
presentRootModal()
151+
}}
152+
id="show-root-modal"
153+
>
154+
Show Root Modal
155+
</IonButton>
156+
137157
<div>Count: {count}</div>
138158
<div>Dismissed with role: {dismissedRole}</div>
139159
<div>Data: {dismissedData && JSON.stringify(dismissedData)}</div>
@@ -143,6 +163,15 @@ const ModalHook: React.FC = () => {
143163
);
144164
};
145165

166+
const ModalSecondary: React.FC = () => {
167+
return (
168+
<div className="ion-padding">
169+
<h1>Secondary Modal</h1>
170+
<p>This text should be visible</p>
171+
</div>
172+
)
173+
}
174+
146175
const MyContext = React.createContext({
147176
value: 'default value',
148177
});

packages/react/test/base/tests/e2e/specs/overlay-hooks/useIonModal.cy.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,16 @@ describe('useIonModal', () => {
7070
//verify context value is overriden value
7171
cy.get('div').contains('overriden value')
7272
});
73+
74+
it('should render nested modal when modals are added and removed at the same time', () => {
75+
cy.get('#show-root-modal').click();
76+
77+
cy.get('ion-modal').should('have.length', 1);
78+
79+
cy.get('ion-modal #show-secondary-modal').click();
80+
81+
cy.get('ion-modal').should('have.length', 1);
82+
83+
cy.get('ion-modal').contains('This text should be visible');
84+
});
7385
});

0 commit comments

Comments
 (0)