diff --git a/packages/react-router/src/ReactRouter/IonRouter.tsx b/packages/react-router/src/ReactRouter/IonRouter.tsx index 0f8f0283a9..1a876aaf47 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 type { Location, NavigationType } from 'react-router-dom'; +import { useLocation, useNavigate, useNavigationType, useParams } from 'react-router-dom'; import { IonRouteInner } from './IonRouteInner'; import { ReactRouterViewStack } from './ReactRouterViewStack'; @@ -21,158 +14,151 @@ export interface LocationState { routerOptions?: { as?: string; unmount?: boolean }; } -interface IonRouteProps extends RouteComponentProps<{}, {}, LocationState> { - registerHistoryListener: (cb: (location: HistoryLocation, action: HistoryAction) => void) => void; -} +function IonRouterInner(props: PropsWithChildren) { + const location = useLocation(); + const params = useParams(); + const navigate = useNavigate(); + const navigationType = useNavigationType(); -interface IonRouteState { - routeInfo: RouteInfo; -} + const [currentTab, setCurrentTab] = useState(); + const [incomingRouteParams, setIncomingRouteParams] = useState>({}); + const [routeInfo, setRouteInfo] = useState(null); + const locationHistory = useRef(new LocationHistory()); -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, - }; - - constructor(props: IonRouteProps) { - super(props); + const viewStack = useRef(new ReactRouterViewStack()); + useEffect(() => { const routeInfo = { id: generateId('routeInfo'), - pathname: this.props.location.pathname, - search: this.props.location.search, + pathname: location.pathname, + search: location.search, }; - 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); + setRouteInfo(routeInfo); + }, []); - this.state = { - routeInfo, - }; - } + useEffect(() => { + handleHistoryChange(location, navigationType); + }, [location, navigationType]); - handleChangeTab(tab: string, path?: string, routeOptions?: any) { + const handleChangeTab = (tab: string, path?: string, routeOptions?: any) => { if (!path) { return; } - const routeInfo = this.locationHistory.getCurrentRouteInfoForTab(tab); + const routeInfo = locationHistory.current.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) { - let leavingLocationInfo: RouteInfo; - if (this.incomingRouteParams) { - if (this.incomingRouteParams.routeAction === 'replace') { - leavingLocationInfo = this.locationHistory.previous(); + if (routeInfo) { + const routeParams = { + ...incomingRouteParams, + routeAction: 'push' as RouteAction, + routeDirection: 'none' as RouterDirection, + }; + if (routeInfo.pathname === pathname) { + routeParams.routeOptions = routeOptions; + setIncomingRouteParams({ + ...routeParams, + routeOptions, + }); + navigate(routeInfo.pathname + (routeInfo.search || '')); } else { - leavingLocationInfo = this.locationHistory.current(); + setIncomingRouteParams({ + ...routeParams, + pathname, + search: search ? '?' + search : undefined, + routeOptions, + }); + navigate(pathname + (search ? '?' + search : '')); } } else { - leavingLocationInfo = this.locationHistory.current(); + handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab); + } + }; + + const handleHistoryChange = (location: Location, action: NavigationType) => { + let leavingLocationInfo: RouteInfo; + if (incomingRouteParams) { + if (incomingRouteParams.routeAction === 'replace') { + leavingLocationInfo = locationHistory.current.previous(); + } else { + leavingLocationInfo = locationHistory.current.current(); + } + } else { + leavingLocationInfo = locationHistory.current.current(); } const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search; if (leavingUrl !== location.pathname) { - if (!this.incomingRouteParams) { + if (!incomingRouteParams) { if (action === 'REPLACE') { - this.incomingRouteParams = { - routeAction: 'replace', - routeDirection: 'none', - tab: this.currentTab, - }; + setIncomingRouteParams({ + routeAction: 'replace' as RouteAction, + routeDirection: 'none' as RouterDirection, + tab: currentTab, + }); } 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); + setIncomingRouteParams({ + ...prevInfo, + routeAction: 'pop' as RouteAction, + routeDirection: 'back' as RouterDirection, + }); } else { - this.incomingRouteParams = { - routeAction: 'pop', - routeDirection: 'none', - tab: this.currentTab, - }; + setIncomingRouteParams({ + routeAction: 'pop' as RouteAction, + routeDirection: 'back' as RouterDirection, + tab: currentTab, + }); } } - if (!this.incomingRouteParams) { - this.incomingRouteParams = { - routeAction: 'push', + if (!incomingRouteParams) { + setIncomingRouteParams({ + routeAction: 'push' as RouteAction, routeDirection: location.state?.direction || 'forward', - routeOptions: location.state?.routerOptions, - tab: this.currentTab, - }; + routeOptions: location.state?.routerOptions, // todo @sean review this routeOptions vs. routerOptions + tab: currentTab, + }); } } let routeInfo: RouteInfo; - if (this.incomingRouteParams?.id) { + if (incomingRouteParams?.id) { routeInfo = { - ...(this.incomingRouteParams as RouteInfo), + ...(incomingRouteParams as RouteInfo), lastPathname: leavingLocationInfo.pathname, }; - this.locationHistory.add(routeInfo); + locationHistory.current.add(routeInfo); } else { - const isPushed = - this.incomingRouteParams.routeAction === 'push' && this.incomingRouteParams.routeDirection === 'forward'; + const isPushed = incomingRouteParams.routeAction === 'push' && incomingRouteParams.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 * /child to /home, we don't want the route info to @@ -190,39 +176,28 @@ class IonRouterInner extends React.PureComponent { routeInfo.routeDirection = currentRouteInfo?.routeDirection || routeInfo.routeDirection; routeInfo.routeAnimation = currentRouteInfo?.routeAnimation || routeInfo.routeAnimation; } - - this.locationHistory.add(routeInfo); + locationHistory.current.add(routeInfo); } - - this.setState({ - routeInfo, - }); + // setState({ routeInfo }); } - this.incomingRouteParams = undefined; - } + setIncomingRouteParams({}); // TODO @sean was undefined + }; - /** - * 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 handleNativeBack = () => { + navigate(-1); + }; - handleNavigate( + const handleNavigate = ( path: string, routeAction: RouteAction, routeDirection?: RouterDirection, routeAnimation?: AnimationBuilder, routeOptions?: any, tab?: string - ) { - this.incomingRouteParams = Object.assign(this.incomingRouteParams || {}, { + ) => { + setIncomingRouteParams({ + ...incomingRouteParams, routeAction, routeDirection, routeOptions, @@ -230,26 +205,24 @@ class IonRouterInner extends React.PureComponent { tab, }); - if (routeAction === 'push') { - this.props.history.push(path); - } else { - this.props.history.replace(path); - } - } + navigate(path, { replace: routeAction !== 'push' }); + }; - handleNavigateBack(defaultHref: string | RouteInfo = '/', routeAnimation?: AnimationBuilder) { + 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.incomingRouteParams = { + setIncomingRouteParams({ ...prevInfo, - routeAction: 'pop', - routeDirection: 'back', + routeAction: 'pop' as RouteAction, + routeDirection: 'back' as RouterDirection, routeAnimation: routeAnimation || routeInfo.routeAnimation, - }; + }); if ( routeInfo.lastPathname === routeInfo.pushedByRoute || /** @@ -260,68 +233,74 @@ 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'); + handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back'); } } else { - this.handleNavigate(defaultHref as string, 'pop', 'back'); + handleNavigate(defaultHref as string, 'pop', 'back'); } } else { - this.handleNavigate(defaultHref as string, 'pop', 'back'); + handleNavigate(defaultHref as string, 'pop', 'back'); } - } + }; - 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 handleResetTab = (tab: string, originalHref: string, originalRouteOptions: any) => { + const firstRouteForTab = locationHistory.current.getFirstRouteInfoForTab(tab); + if (firstRouteForTab) { + const routeInfo = { + ...firstRouteForTab, + pathName: originalHref, + routeOptions: originalRouteOptions, + routeAction: 'pop' as RouteAction, + routeDirection: 'back' as RouterDirection, + }; + setIncomingRouteParams(routeInfo); + navigate(routeInfo.pathname + (routeInfo.search || '')); } - } + }; - handleSetCurrentTab(tab: string) { - this.currentTab = tab; - const ri = { ...this.locationHistory.current() }; - if (ri.tab !== tab) { - ri.tab = tab; - this.locationHistory.update(ri); + const handleSetCurrentTab = (tab: string) => { + setCurrentTab(tab); + const currentRoute = locationHistory.current.current(); + if (currentRoute && currentRoute.tab !== tab) { + const updatedRoute = { ...currentRoute, tab }; + locationHistory.current.update(updatedRoute); } - } + }; - render() { - return ( - - - {this.props.children} - - - ); - } + const routeManagerContextValue: 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, + }; + + return ( + + + {props.children} + + + ); } -export const IonRouter = withRouter(IonRouterInner); -IonRouter.displayName = 'IonRouter'; +export const IonRouter = IonRouterInner;