chore: migrate IonRouter to react-router v6

This commit is contained in:
Sean Perkins
2023-06-29 01:14:17 -04:00
parent 16c373f853
commit 1db7c5322c

View File

@@ -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<any>, action: HistoryAction) => void) => void;
}
function IonRouterInner(props: PropsWithChildren<any>) {
const location = useLocation();
const params = useParams();
const navigate = useNavigate();
const navigationType = useNavigationType();
interface IonRouteState {
routeInfo: RouteInfo;
}
const [currentTab, setCurrentTab] = useState<string>();
const [incomingRouteParams, setIncomingRouteParams] = useState<Partial<RouteInfo>>({});
const [routeInfo, setRouteInfo] = useState<any>(null);
const locationHistory = useRef(new LocationHistory());
class IonRouterInner extends React.PureComponent<IonRouteProps, IonRouteState> {
currentTab?: string;
exitViewFromOtherOutletHandlers: ((pathname: string) => ViewItem | undefined)[] = [];
incomingRouteParams?: Partial<RouteInfo>;
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<LocationState>, 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<IonRouteProps, IonRouteState> {
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<IonRouteProps, IonRouteState> {
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<IonRouteProps, IonRouteState> {
*/
(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 (
<RouteManagerContext.Provider value={this.routeMangerContextState}>
<NavManager
ionRoute={IonRouteInner}
ionRedirect={{}}
stackManager={StackManager}
routeInfo={this.state.routeInfo!}
onNativeBack={this.handleNativeBack}
onNavigateBack={this.handleNavigateBack}
onNavigate={this.handleNavigate}
onSetCurrentTab={this.handleSetCurrentTab}
onChangeTab={this.handleChangeTab}
onResetTab={this.handleResetTab}
locationHistory={this.locationHistory}
>
{this.props.children}
</NavManager>
</RouteManagerContext.Provider>
);
}
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 (
<RouteManagerContext.Provider value={routeManagerContextValue}>
<NavManager
ionRoute={IonRouteInner}
ionRedirect={{}}
stackManager={StackManager}
routeInfo={routeInfo}
onNativeBack={handleNativeBack}
onNavigateBack={handleNavigateBack}
onNavigate={handleNavigate}
onSetCurrentTab={handleSetCurrentTab}
onChangeTab={handleChangeTab}
onResetTab={handleResetTab}
locationHistory={locationHistory.current}
>
{props.children}
</NavManager>
</RouteManagerContext.Provider>
);
}
export const IonRouter = withRouter(IonRouterInner);
IonRouter.displayName = 'IonRouter';
export const IonRouter = IonRouterInner;