-
Notifications
You must be signed in to change notification settings - Fork 13.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(react): inline overlays dismiss when parent component unmounts #26245
Merged
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
678d34e
fix(react): inline overlays dismiss when parent component unmounts
sean-perkins 1ea0cd2
test(react): navigate to have history for pop operation
sean-perkins e345def
fix: memory leak with event handlers
sean-perkins e4294cd
Merge branch 'main' into fix/react-overlays
sean-perkins 7d8b091
chore: remove duplicate className from merge
sean-perkins 25dbfa9
Merge remote-tracking branch 'origin/main' into fix/react-overlays
sean-perkins 7cd912b
Merge branch 'main' into fix/react-overlays
sean-perkins 3422543
Merge branch 'main' into fix/react-overlays
sean-perkins d757a0b
Merge branch 'main' into fix/react-overlays
sean-perkins 6ada9f6
Merge branch 'main' into fix/react-overlays
sean-perkins 12ad13c
Merge branch 'main' into fix/react-overlays
sean-perkins 93b201a
fix: detach event listeners on unmount
sean-perkins 811e869
docs: event handler from event store
sean-perkins 50287a8
chore: remove local event listener before dismiss
sean-perkins e315da5
Merge branch 'main' into fix/react-overlays
sean-perkins 4e68e60
fix: life cycle methods are manually called on unmounted overlays
sean-perkins f6d5843
Merge remote-tracking branch 'origin/fix/react-overlays' into fix/rea…
sean-perkins 2ac9857
chore: lint formatting
sean-perkins b4f4e26
Merge remote-tracking branch 'origin/main' into fix/react-overlays
sean-perkins a521103
Merge branch 'main' into fix/react-overlays
sean-perkins bc3528a
Merge remote-tracking branch 'origin/main' into fix/react-overlays
sean-perkins a0af097
fix: skip dismiss transition when component is force unmounted
sean-perkins b29bd02
fix: import type for @ionic/core/components
sean-perkins dafb6ac
chore: prettier
sean-perkins 17ad0d6
fix: remove element node
sean-perkins 9520826
chore: typo in comment
sean-perkins baefa2f
Merge branch 'main' into fix/react-overlays
sean-perkins 0e6e3c5
chore: update comment to apply to all overlays
sean-perkins dd6d041
Merge remote-tracking branch 'origin/main' into fix/react-overlays
sean-perkins 006270b
Merge branch 'main' into fix/react-overlays
sean-perkins 0404461
Merge branch 'main' into fix/react-overlays
sean-perkins File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
packages/react-router/test-app/src/pages/overlays/Overlays.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { IonButton, IonContent, IonModal } from '@ionic/react'; | ||
import { useState } from 'react'; | ||
import { useHistory } from 'react-router'; | ||
|
||
const Overlays: React.FC = () => { | ||
const [isOpen, setIsOpen] = useState(false); | ||
|
||
const history = useHistory(); | ||
|
||
const goBack = () => history.goBack(); | ||
const replace = () => history.replace('/'); | ||
const push = () => history.push('/'); | ||
|
||
return ( | ||
<> | ||
<IonButton id="openModal" onClick={() => setIsOpen(true)}> | ||
Open Modal | ||
</IonButton> | ||
<IonModal | ||
isOpen={isOpen} | ||
onDidDismiss={() => { | ||
setIsOpen(false); | ||
}} | ||
> | ||
<IonContent> | ||
<IonButton id="goBack" onClick={goBack}> | ||
Go Back | ||
</IonButton> | ||
<IonButton id="replace" onClick={replace}> | ||
Replace | ||
</IonButton> | ||
<IonButton id="push" onClick={push}> | ||
Push | ||
</IonButton> | ||
</IonContent> | ||
</IonModal> | ||
</> | ||
); | ||
}; | ||
|
||
export default Overlays; |
41 changes: 41 additions & 0 deletions
41
packages/react-router/test-app/tests/e2e/specs/overlays.cy.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
const port = 3000; | ||
|
||
describe('Overlays', () => { | ||
it('should remove the overlay when going back to the previous route', () => { | ||
// Requires navigation history to perform a pop | ||
cy.visit(`http://localhost:${port}`); | ||
cy.visit(`http://localhost:${port}/overlays`); | ||
|
||
cy.get('#openModal').click(); | ||
|
||
cy.get('ion-modal').should('exist'); | ||
|
||
cy.get('#goBack').click(); | ||
|
||
cy.get('ion-modal').should('not.exist'); | ||
}); | ||
|
||
it('should remove the overlay when pushing to a new route', () => { | ||
cy.visit(`http://localhost:${port}/overlays`); | ||
|
||
cy.get('#openModal').click(); | ||
|
||
cy.get('ion-modal').should('exist'); | ||
|
||
cy.get('#push').click(); | ||
|
||
cy.get('ion-modal').should('not.exist'); | ||
}); | ||
|
||
it('should remove the overlay when replacing the route', () => { | ||
cy.visit(`http://localhost:${port}/overlays`); | ||
|
||
cy.get('#openModal').click(); | ||
|
||
cy.get('ion-modal').should('exist'); | ||
|
||
cy.get('#replace').click(); | ||
|
||
cy.get('ion-modal').should('not.exist'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { isCoveredByReact } from '../react-component-lib/utils'; | ||
|
||
/** | ||
* The @stencil/react-output-target will bind event listeners for any | ||
* attached props that use the `on` prefix. This function will remove | ||
* those event listeners when the component is unmounted. | ||
* | ||
* This prevents memory leaks and React state updates on unmounted components. | ||
*/ | ||
export const detachProps = (node: HTMLElement, props: any) => { | ||
if (node instanceof Element) { | ||
Object.keys(props).forEach((name) => { | ||
if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { | ||
const eventName = name.substring(2); | ||
const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1); | ||
if (!isCoveredByReact(eventNameLc)) { | ||
/** | ||
* Detach custom event bindings (not built-in React events) | ||
* that were added by the @stencil/react-output-target attachProps function. | ||
*/ | ||
detachEvent(node, eventNameLc); | ||
} | ||
} | ||
}); | ||
} | ||
}; | ||
|
||
const detachEvent = ( | ||
node: Element & { __events?: { [key: string]: ((e: Event) => any) | undefined } }, | ||
eventName: string | ||
) => { | ||
const eventStore = node.__events || (node.__events = {}); | ||
/** | ||
* If the event listener was added by attachProps, it will | ||
* be stored in the __events object. | ||
*/ | ||
const eventHandler = eventStore[eventName]; | ||
if (eventHandler) { | ||
node.removeEventListener(eventName, eventHandler); | ||
eventStore[eventName] = undefined; | ||
} | ||
}; |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we only remove the
didDismiss
event?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
createInlineOverlayComponent
binds two internal event listeners fordidDismiss
andwillPresent
. Both implementations have an internal state update for:At this point in the lifecycle of the component the component is unmounting.
willPresent
cannot fire. However,didDismiss
can fire and cause any user-implemented callback handlers foronDidDismiss
to run after the React component has unmounted. If this occurs, React will throw an exception in the console about a memory leak.This code disconnects the internal event listener manually, before the element can dispatch
didDismiss
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would
detachProps
handle removing that listener? If so, would it make sense to instead calldetachProps
beforenode.remove
and then remove this line?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
detachProps
only removes event listeners added byattachProps
(because we track the events on the element node on a key called__events
). This specific event listener is manually added within the React class and isn't removed as a result ofdetachProps
.