fix(react): Nav unmounts component while invoking popTo or popToRoot (#27821)

Issue number: Resolves #27798

---------

## What is the current behavior

React IonNav component's views are missing keys, leading to unnecessary
duplicate mounting of components.


## What is the new behavior?
- Adds key to views of React IonNav component.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

---------

Co-authored-by: Sean Perkins <sean@ionic.io>
This commit is contained in:
zhbhun
2023-09-21 01:31:58 +08:00
committed by GitHub
parent 7b197a3226
commit 0edcb2cd85
3 changed files with 42 additions and 8 deletions

View File

@ -1,6 +1,8 @@
import type { FrameworkDelegate } from '@ionic/core/components'; import type { FrameworkDelegate } from '@ionic/core/components';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { generateId } from './utils/generateId';
// TODO(FW-2959): types // TODO(FW-2959): types
type ReactComponent = (props?: any) => JSX.Element; type ReactComponent = (props?: any) => JSX.Element;
@ -10,6 +12,9 @@ export const ReactDelegate = (
removeView: (view: React.ReactElement) => void removeView: (view: React.ReactElement) => void
): FrameworkDelegate => { ): FrameworkDelegate => {
const refMap = new WeakMap<HTMLElement, React.ReactElement>(); const refMap = new WeakMap<HTMLElement, React.ReactElement>();
const reactDelegateId = `react-delegate-${generateId()}`;
// Incrementing counter to generate unique keys for each view
let id = 0;
const attachViewToDom = async ( const attachViewToDom = async (
parentElement: HTMLElement, parentElement: HTMLElement,
@ -22,7 +27,8 @@ export const ReactDelegate = (
parentElement.appendChild(div); parentElement.appendChild(div);
const componentWithProps = component(propsOrDataObj); const componentWithProps = component(propsOrDataObj);
const hostComponent = createPortal(componentWithProps, div); const key = `${reactDelegateId}-${id++}`;
const hostComponent = createPortal(componentWithProps, div, key);
refMap.set(div, hostComponent); refMap.set(div, hostComponent);

View File

@ -11,7 +11,7 @@ import {
IonBackButton, IonBackButton,
IonPage, IonPage,
} from '@ionic/react'; } from '@ionic/react';
import React, { useRef } from 'react'; import React, { useEffect, useRef } from 'react';
const PageOne = ({ const PageOne = ({
nav, nav,
@ -39,7 +39,10 @@ const PageOne = ({
<IonNavLink <IonNavLink
routerDirection="forward" routerDirection="forward"
component={PageTwo} component={PageTwo}
componentProps={{ someValue: 'Hello' }} componentProps={{
someValue: 'Hello',
nav: nav,
}}
> >
<IonButton>Go to Page Two</IonButton> <IonButton>Go to Page Two</IonButton>
</IonNavLink> </IonNavLink>
@ -48,7 +51,7 @@ const PageOne = ({
); );
}; };
const PageTwo = (props?: { someValue: string }) => { const PageTwo = ({ nav, ...rest }: { someValue: string; nav: React.MutableRefObject<HTMLIonNavElement> }) => {
return ( return (
<> <>
<IonHeader> <IonHeader>
@ -61,8 +64,8 @@ const PageTwo = (props?: { someValue: string }) => {
</IonHeader> </IonHeader>
<IonContent id="pageTwoContent"> <IonContent id="pageTwoContent">
<IonLabel>Page two content</IonLabel> <IonLabel>Page two content</IonLabel>
<div id="pageTwoProps">{JSON.stringify(props)}</div> <div id="pageTwoProps">{JSON.stringify(rest)}</div>
<IonNavLink routerDirection="forward" component={PageThree}> <IonNavLink routerDirection="forward" component={() => <PageThree nav={nav} />}>
<IonButton>Go to Page Three</IonButton> <IonButton>Go to Page Three</IonButton>
</IonNavLink> </IonNavLink>
</IonContent> </IonContent>
@ -70,7 +73,12 @@ const PageTwo = (props?: { someValue: string }) => {
); );
}; };
const PageThree = () => { const PageThree = ({ nav }: { nav: React.MutableRefObject<HTMLIonNavElement> }) => {
useEffect(() => {
return () => {
window.dispatchEvent(new CustomEvent('pageThreeUnmounted'));
};
});
return ( return (
<> <>
<IonHeader> <IonHeader>
@ -81,8 +89,9 @@ const PageThree = () => {
</IonButtons> </IonButtons>
</IonToolbar> </IonToolbar>
</IonHeader> </IonHeader>
<IonContent> <IonContent id="pageThreeContent">
<IonLabel>Page three content</IonLabel> <IonLabel>Page three content</IonLabel>
<IonButton onClick={() => nav.current.popToRoot()}>popToRoot</IonButton>
</IonContent> </IonContent>
</> </>
); );

View File

@ -47,4 +47,23 @@ describe('IonNav', () => {
cy.get('#pageTwoProps').should('have.text', '{"someValue":"Hello"}'); cy.get('#pageTwoProps').should('have.text', '{"someValue":"Hello"}');
}); });
it('should unmount pages when popping to root', () => {
// Issue: https://github.com/ionic-team/ionic-framework/issues/27798
cy.contains('Go to Page Two').click();
cy.get('#pageTwoContent').should('be.visible');
cy.contains('Go to Page Three').click();
cy.get('#pageThreeContent').should('be.visible');
cy.window().then((window) => {
window.addEventListener('pageThreeUnmounted', cy.stub().as('pageThreeUnmounted'));
});
cy.get('ion-button').contains('popToRoot').click();
cy.get('#pageThreeContent').should('not.exist');
cy.get('@pageThreeUnmounted').should('have.been.calledOnce');
});
}); });