diff --git a/packages/react-router/src/ReactRouter/IonRouter.tsx b/packages/react-router/src/ReactRouter/IonRouter.tsx index fcadd6561a..aafc565401 100644 --- a/packages/react-router/src/ReactRouter/IonRouter.tsx +++ b/packages/react-router/src/ReactRouter/IonRouter.tsx @@ -1,16 +1,9 @@ -import type { - AnimationBuilder, - RouteAction, - RouteInfo, - RouteManagerContextState, - RouterDirection, - ViewItem, -} from '@ionic/react'; +import type { AnimationBuilder, RouteAction, RouteInfo, RouteManagerContextState, RouterDirection } from '@ionic/react'; import { LocationHistory, NavManager, RouteManagerContext, generateId, getConfig } from '@ionic/react'; import type { Action as HistoryAction, Location as HistoryLocation } from 'history'; -import React from 'react'; -import type { RouteComponentProps } from 'react-router-dom'; -import { withRouter } from 'react-router-dom'; +import type { PropsWithChildren } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { IonRouteInner } from './IonRouteInner'; import { ReactRouterViewStack } from './ReactRouterViewStack'; @@ -21,157 +14,110 @@ export interface LocationState { routerOptions?: { as?: string; unmount?: boolean }; } -interface IonRouteProps extends RouteComponentProps<{}, {}, LocationState> { +interface IonRouterProps { registerHistoryListener: (cb: (location: HistoryLocation, action: HistoryAction) => void) => void; } -interface IonRouteState { - routeInfo: RouteInfo; -} +export const IonRouter = ({ children, registerHistoryListener }: PropsWithChildren) => { + const location = useLocation(); + const params = useParams(); + const navigate = useNavigate(); -class IonRouterInner extends React.PureComponent { - currentTab?: string; - exitViewFromOtherOutletHandlers: ((pathname: string) => ViewItem | undefined)[] = []; - incomingRouteParams?: Partial; - locationHistory = new LocationHistory(); - viewStack = new ReactRouterViewStack(); - routeMangerContextState: RouteManagerContextState = { - canGoBack: () => this.locationHistory.canGoBack(), - clearOutlet: this.viewStack.clear, - findViewItemByPathname: this.viewStack.findViewItemByPathname, - getChildrenToRender: this.viewStack.getChildrenToRender, - goBack: () => this.handleNavigateBack(), - createViewItem: this.viewStack.createViewItem, - findViewItemByRouteInfo: this.viewStack.findViewItemByRouteInfo, - findLeavingViewItemByRouteInfo: this.viewStack.findLeavingViewItemByRouteInfo, - addViewItem: this.viewStack.add, - unMountViewItem: this.viewStack.remove, - }; + const didMountRef = useRef(false); + const locationHistory = useRef(new LocationHistory()); + const currentTab = useRef(undefined); + const viewStack = useRef(new ReactRouterViewStack()); + const incomingRouteParams = useRef | null>(null); - constructor(props: IonRouteProps) { - super(props); + const [routeInfo, setRouteInfo] = useState({ + id: generateId('routeInfo'), + pathname: location.pathname, + search: location.search, + }); - const routeInfo = { - id: generateId('routeInfo'), - pathname: this.props.location.pathname, - search: this.props.location.search, - }; + useEffect(() => { + didMountRef.current = true; + }, []); - this.locationHistory.add(routeInfo); - this.handleChangeTab = this.handleChangeTab.bind(this); - this.handleResetTab = this.handleResetTab.bind(this); - this.handleNativeBack = this.handleNativeBack.bind(this); - this.handleNavigate = this.handleNavigate.bind(this); - this.handleNavigateBack = this.handleNavigateBack.bind(this); - this.props.registerHistoryListener(this.handleHistoryChange.bind(this)); - this.handleSetCurrentTab = this.handleSetCurrentTab.bind(this); - - this.state = { - routeInfo, - }; - } - - handleChangeTab(tab: string, path?: string, routeOptions?: any) { - if (!path) { - return; - } - - const routeInfo = this.locationHistory.getCurrentRouteInfoForTab(tab); - const [pathname, search] = path.split('?'); - if (routeInfo) { - this.incomingRouteParams = { ...routeInfo, routeAction: 'push', routeDirection: 'none' }; - if (routeInfo.pathname === pathname) { - this.incomingRouteParams.routeOptions = routeOptions; - this.props.history.push(routeInfo.pathname + (routeInfo.search || '')); - } else { - this.incomingRouteParams.pathname = pathname; - this.incomingRouteParams.search = search ? '?' + search : undefined; - this.incomingRouteParams.routeOptions = routeOptions; - this.props.history.push(pathname + (search ? '?' + search : '')); - } - } else { - this.handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab); - } - } - - handleHistoryChange(location: HistoryLocation, action: HistoryAction) { + const handleHistoryChange = (location: HistoryLocation, action: HistoryAction) => { let leavingLocationInfo: RouteInfo; - if (this.incomingRouteParams) { - if (this.incomingRouteParams.routeAction === 'replace') { - leavingLocationInfo = this.locationHistory.previous(); + if (incomingRouteParams) { + if (incomingRouteParams.current?.routeAction === 'replace') { + leavingLocationInfo = locationHistory.current.previous(); } else { - leavingLocationInfo = this.locationHistory.current(); + leavingLocationInfo = locationHistory.current.current(); } } else { - leavingLocationInfo = this.locationHistory.current(); + leavingLocationInfo = locationHistory.current.current(); } const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search; if (leavingUrl !== location.pathname) { - if (!this.incomingRouteParams) { + if (!incomingRouteParams.current) { if (action === 'REPLACE') { - this.incomingRouteParams = { + incomingRouteParams.current = { routeAction: 'replace', routeDirection: 'none', - tab: this.currentTab, + tab: currentTab.current, }; } if (action === 'POP') { - const currentRoute = this.locationHistory.current(); + const currentRoute = locationHistory.current.current(); if (currentRoute && currentRoute.pushedByRoute) { - const prevInfo = this.locationHistory.findLastLocation(currentRoute); - this.incomingRouteParams = { ...prevInfo, routeAction: 'pop', routeDirection: 'back' }; + const prevInfo = locationHistory.current.findLastLocation(currentRoute); + incomingRouteParams.current = { ...prevInfo, routeAction: 'pop', routeDirection: 'back' }; } else { - this.incomingRouteParams = { + incomingRouteParams.current = { routeAction: 'pop', routeDirection: 'none', - tab: this.currentTab, + tab: currentTab.current, }; } } - if (!this.incomingRouteParams) { - this.incomingRouteParams = { + if (!incomingRouteParams.current) { + incomingRouteParams.current = { routeAction: 'push', routeDirection: location.state?.direction || 'forward', routeOptions: location.state?.routerOptions, - tab: this.currentTab, + tab: currentTab.current, }; } } let routeInfo: RouteInfo; - if (this.incomingRouteParams?.id) { + if (incomingRouteParams.current?.id) { routeInfo = { - ...(this.incomingRouteParams as RouteInfo), + ...(incomingRouteParams.current as RouteInfo), lastPathname: leavingLocationInfo.pathname, }; - this.locationHistory.add(routeInfo); + locationHistory.current.add(routeInfo); } else { const isPushed = - this.incomingRouteParams.routeAction === 'push' && this.incomingRouteParams.routeDirection === 'forward'; + incomingRouteParams.current?.routeAction === 'push' && + incomingRouteParams.current.routeDirection === 'forward'; routeInfo = { id: generateId('routeInfo'), - ...this.incomingRouteParams, + ...incomingRouteParams, lastPathname: leavingLocationInfo.pathname, pathname: location.pathname, search: location.search, - params: this.props.match.params, + params: params as any, // TODO @sean fix type of route info for params prevRouteLastPathname: leavingLocationInfo.lastPathname, }; if (isPushed) { routeInfo.tab = leavingLocationInfo.tab; routeInfo.pushedByRoute = leavingLocationInfo.pathname; } else if (routeInfo.routeAction === 'pop') { - const r = this.locationHistory.findLastLocation(routeInfo); + const r = locationHistory.current.findLastLocation(routeInfo); routeInfo.pushedByRoute = r?.pushedByRoute; } else if (routeInfo.routeAction === 'push' && routeInfo.tab !== leavingLocationInfo.tab) { // If we are switching tabs grab the last route info for the tab and use its pushedByRoute - const lastRoute = this.locationHistory.getCurrentRouteInfoForTab(routeInfo.tab); + const lastRoute = locationHistory.current.getCurrentRouteInfoForTab(routeInfo.tab); routeInfo.pushedByRoute = lastRoute?.pushedByRoute; } else if (routeInfo.routeAction === 'replace') { // Make sure to set the lastPathname, etc.. to the current route so the page transitions out - const currentRouteInfo = this.locationHistory.current(); + const currentRouteInfo = locationHistory.current.current(); /** * If going from /home to /child, then replacing from @@ -198,58 +144,79 @@ class IonRouterInner extends React.PureComponent { routeInfo.routeAnimation = routeInfo.routeAnimation || currentRouteInfo?.routeAnimation; } - this.locationHistory.add(routeInfo); + locationHistory.current.add(routeInfo); } - - this.setState({ - routeInfo, - }); + setRouteInfo(routeInfo); } - this.incomingRouteParams = undefined; - } + incomingRouteParams.current = null; + }; - /** - * history@4.x uses goBack(), history@5.x uses back() - * TODO: If support for React Router <=5 is dropped - * this logic is no longer needed. We can just - * assume back() is available. - */ - handleNativeBack() { - const history = this.props.history as any; - const goBack = history.goBack || history.back; - goBack(); - } + const handleResetTab = (tab: string, originalHref: string, originalRouteOptions: any) => { + const routeInfo = locationHistory.current.getFirstRouteInfoForTab(tab); + if (routeInfo) { + const newRouteInfo = { ...routeInfo }; + newRouteInfo.pathname = originalHref; + newRouteInfo.routeOptions = originalRouteOptions; + incomingRouteParams.current = { ...newRouteInfo, routeAction: 'pop', routeDirection: 'back' }; + navigate(newRouteInfo.pathname + (newRouteInfo.search || '')); + } + }; - handleNavigate( - path: string, - routeAction: RouteAction, - routeDirection?: RouterDirection, - routeAnimation?: AnimationBuilder, - routeOptions?: any, - tab?: string - ) { - this.incomingRouteParams = Object.assign(this.incomingRouteParams || {}, { - routeAction, - routeDirection, - routeOptions, - routeAnimation, - tab, - }); + const handleChangeTab = (tab: string, path?: string, routeOptions?: any) => { + if (!path) { + return; + } - if (routeAction === 'push') { - this.props.history.push(path); + const routeInfo = locationHistory.current.getCurrentRouteInfoForTab(tab); + const [pathname, search] = path.split('?'); + if (routeInfo) { + const routeParams = { + ...routeInfo, + routeAction: 'push' as RouteAction, + routeDirection: 'none' as RouterDirection, + }; + if (routeInfo.pathname === pathname) { + incomingRouteParams.current = { + ...routeParams, + routeOptions, + }; + + navigate(routeInfo.pathname + (routeInfo.search || '')); + } else { + incomingRouteParams.current = { + ...routeParams, + pathname, + search: search ? '?' + search : undefined, + routeOptions, + }; + + navigate(pathname + (search ? '?' + search : '')); + } } else { - this.props.history.replace(path); + handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab); } - } + }; - handleNavigateBack(defaultHref: string | RouteInfo = '/', routeAnimation?: AnimationBuilder) { + const handleSetCurrentTab = (tab: string) => { + currentTab.current = tab; + const ri = { ...locationHistory.current.current() }; + if (ri.tab !== tab) { + ri.tab = tab; + locationHistory.current.update(ri); + } + }; + + const handleNativeBack = () => { + navigate(-1); + }; + + const handleNavigateBack = (defaultHref: string | RouteInfo = '/', routeAnimation?: AnimationBuilder) => { const config = getConfig(); defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref' as any); - const routeInfo = this.locationHistory.current(); + const routeInfo = locationHistory.current.current(); if (routeInfo && routeInfo.pushedByRoute) { - const prevInfo = this.locationHistory.findLastLocation(routeInfo); + const prevInfo = locationHistory.current.findLastLocation(routeInfo); if (prevInfo) { /** * This needs to be passed to handleNavigate @@ -257,7 +224,7 @@ class IonRouterInner extends React.PureComponent { * will be overridden. */ const incomingAnimation = routeAnimation || routeInfo.routeAnimation; - this.incomingRouteParams = { + incomingRouteParams.current = { ...prevInfo, routeAction: 'pop', routeDirection: 'back', @@ -273,68 +240,75 @@ class IonRouterInner extends React.PureComponent { */ (prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab === '' && prevInfo.tab === '') ) { - /** - * history@4.x uses goBack(), history@5.x uses back() - * TODO: If support for React Router <=5 is dropped - * this logic is no longer needed. We can just - * assume back() is available. - */ - const history = this.props.history as any; - const goBack = history.goBack || history.back; - goBack(); + navigate(-1); } else { - this.handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation); + handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', incomingAnimation); } } else { - this.handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation); + handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation); } } else { - this.handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation); + handleNavigate(defaultHref as string, 'pop', 'back', routeAnimation); } + }; + + const handleNavigate = ( + path: string, + routeAction: RouteAction, + routeDirection?: RouterDirection, + routeAnimation?: AnimationBuilder, + routeOptions?: any, + tab?: string + ) => { + incomingRouteParams.current = Object.assign(incomingRouteParams.current || {}, { + routeAction, + routeDirection, + routeOptions, + routeAnimation, + tab, + }); + + navigate(path, { replace: routeAction !== 'push' }); + }; + + if (!didMountRef.current) { + locationHistory.current.add(routeInfo); + + registerHistoryListener(handleHistoryChange); } - handleResetTab(tab: string, originalHref: string, originalRouteOptions: any) { - const routeInfo = this.locationHistory.getFirstRouteInfoForTab(tab); - if (routeInfo) { - const newRouteInfo = { ...routeInfo }; - newRouteInfo.pathname = originalHref; - newRouteInfo.routeOptions = originalRouteOptions; - this.incomingRouteParams = { ...newRouteInfo, routeAction: 'pop', routeDirection: 'back' }; - this.props.history.push(newRouteInfo.pathname + (newRouteInfo.search || '')); - } - } + const routeMangerContextValue: RouteManagerContextState = { + canGoBack: () => locationHistory.current.canGoBack(), + clearOutlet: viewStack.current.clear, + findViewItemByPathname: viewStack.current.findViewItemByPathname, + getChildrenToRender: viewStack.current.getChildrenToRender, + goBack: () => handleNavigateBack(), + createViewItem: viewStack.current.createViewItem, + findViewItemByRouteInfo: viewStack.current.findViewItemByRouteInfo, + findLeavingViewItemByRouteInfo: viewStack.current.findLeavingViewItemByRouteInfo, + addViewItem: viewStack.current.add, + unMountViewItem: viewStack.current.remove, + }; - handleSetCurrentTab(tab: string) { - this.currentTab = tab; - const ri = { ...this.locationHistory.current() }; - if (ri.tab !== tab) { - ri.tab = tab; - this.locationHistory.update(ri); - } - } + return ( + + + {children} + + + ); +}; - render() { - return ( - - - {this.props.children} - - - ); - } -} - -export const IonRouter = withRouter(IonRouterInner); IonRouter.displayName = 'IonRouter';