chore(react-router): cleaning up util files

This commit is contained in:
ShaneK
2025-12-02 11:49:00 -08:00
parent 71e55addf9
commit 418ac75501
9 changed files with 132 additions and 141 deletions

View File

@@ -11,11 +11,11 @@ import React from 'react';
import type { PathMatch } from 'react-router';
import { Navigate, UNSAFE_RouteContext as RouteContext } from 'react-router-dom';
import { analyzeRouteChildren, computeParentPath, extractRouteChildren } from './utils/computeParentPath';
import { derivePathnameToMatch } from './utils/derivePathnameToMatch';
import { matchPath } from './utils/matchPath';
import { normalizePathnameForComparison } from './utils/normalizePath';
import { isNavigateElement, sortViewsBySpecificity } from './utils/routeUtils';
import { analyzeRouteChildren, computeParentPath } from './utils/computeParentPath';
import { derivePathnameToMatch, matchPath } from './utils/pathMatching';
import { normalizePathnameForComparison } from './utils/pathNormalization';
import { extractRouteChildren, isNavigateElement } from './utils/routeElements';
import { sortViewsBySpecificity } from './utils/viewItemUtils';
/**
* Delay in milliseconds before removing a Navigate view item after a redirect.
@@ -51,7 +51,6 @@ const createDefaultMatch = (
};
};
const computeRelativeToParent = (pathname: string, parentPath?: string): string | null => {
if (!parentPath) return null;
const normalizedParent = normalizePathnameForComparison(parentPath);
@@ -238,7 +237,6 @@ export class ReactRouterViewStack extends ViewStacks {
// Flag to indicate this view should not be reused for this different parameterized path
const shouldSkipForDifferentParam = isParameterRoute && match && previousMatch && !isSamePath;
// Don't deactivate views automatically - let the StackManager handle view lifecycle
// This preserves views in the stack for navigation history like native apps
// Views will be hidden/shown by the StackManager's transition logic instead of being unmounted
@@ -519,8 +517,7 @@ export class ReactRouterViewStack extends ViewStacks {
// Check if current pathname is within this view's route hierarchy
const isWithinRouteHierarchy =
normalizedCurrentPath === normalizedViewPath ||
normalizedCurrentPath.startsWith(normalizedViewPath + '/');
normalizedCurrentPath === normalizedViewPath || normalizedCurrentPath.startsWith(normalizedViewPath + '/');
if (!isWithinRouteHierarchy) {
// View is outside current route hierarchy, remove it

View File

@@ -10,17 +10,10 @@ import React from 'react';
import { Route } from 'react-router-dom';
import { clonePageElement } from './clonePageElement';
import {
analyzeRouteChildren,
computeCommonPrefix,
computeParentPath,
extractRouteChildren,
} from './utils/computeParentPath';
import { derivePathnameToMatch } from './utils/derivePathnameToMatch';
import { getRoutesChildren } from './utils/getRoutesChildren';
import { matchPath } from './utils/matchPath';
import { stripTrailingSlash } from './utils/normalizePath';
import { isNavigateElement } from './utils/routeUtils';
import { analyzeRouteChildren, computeCommonPrefix, computeParentPath } from './utils/computeParentPath';
import { derivePathnameToMatch, matchPath } from './utils/pathMatching';
import { stripTrailingSlash } from './utils/pathNormalization';
import { extractRouteChildren, getRoutesChildren, isNavigateElement } from './utils/routeElements';
/**
* Delay in milliseconds before unmounting a view after a transition completes.
@@ -149,7 +142,7 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
enteringViewItem: ViewItem | undefined;
leavingViewItem: ViewItem | undefined;
} {
let enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
const enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
let leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id);
// If we don't have a leaving view item, but the route info indicates
@@ -257,7 +250,8 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
return false;
}
const routesChildren = getRoutesChildren(this.ionRouterOutlet.props.children) ?? this.ionRouterOutlet.props.children;
const routesChildren =
getRoutesChildren(this.ionRouterOutlet.props.children) ?? this.ionRouterOutlet.props.children;
const routeChildren = React.Children.toArray(routesChildren).filter(
(child): child is React.ReactElement => React.isValidElement(child) && child.type === Route
);
@@ -382,11 +376,7 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
/**
* Handles the delayed unmount of the leaving view item after a replace action.
*/
private handleLeavingViewUnmount(
routeInfo: RouteInfo,
enteringViewItem: ViewItem,
leavingViewItem: ViewItem
): void {
private handleLeavingViewUnmount(routeInfo: RouteInfo, enteringViewItem: ViewItem, leavingViewItem: ViewItem): void {
if (routeInfo.routeAction !== 'replace' || !leavingViewItem.ionPageElement) {
return;
}
@@ -538,9 +528,7 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
// the nested outlet's componentDidUpdate won't be called, so we must hide
// the ion-page elements here to prevent them from remaining visible on top
// of other content after navigation to a different route.
const allViewsInOutlet = this.context.getViewItemsForOutlet
? this.context.getViewItemsForOutlet(this.id)
: [];
const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
allViewsInOutlet.forEach((viewItem) => {
hideIonPageElement(viewItem.ionPageElement);
});
@@ -570,7 +558,9 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
}
// Find entering and leaving view items
let { enteringViewItem, leavingViewItem } = this.findViewItems(routeInfo);
const viewItems = this.findViewItems(routeInfo);
let enteringViewItem = viewItems.enteringViewItem;
const leavingViewItem = viewItems.leavingViewItem;
const shouldUnmountLeavingViewItem = this.shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem);
// Get parent path for nested outlets

View File

@@ -1,8 +1,6 @@
import React from 'react';
import { Route } from 'react-router-dom';
import type React from 'react';
import { getRoutesChildren } from './getRoutesChildren';
import { matchPath } from './matchPath';
import { matchPath } from './pathMatching';
/**
* Finds the longest common prefix among an array of paths.
@@ -76,19 +74,6 @@ export interface ParentPathResult {
outletMountPath: string | undefined;
}
/**
* Extracts Route children from a node (either directly or from a Routes wrapper).
*
* @param children The children to extract routes from.
* @returns An array of Route elements.
*/
export const extractRouteChildren = (children: React.ReactNode): React.ReactElement[] => {
const routesChildren = getRoutesChildren(children) ?? children;
return React.Children.toArray(routesChildren).filter(
(child): child is React.ReactElement => React.isValidElement(child) && child.type === Route
);
};
interface RouteAnalysis {
hasRelativeRoutes: boolean;
hasIndexRoute: boolean;

View File

@@ -1,57 +0,0 @@
/**
* Determines the portion of a pathname that a given route pattern should match against.
* For absolute route patterns we return the full pathname. For relative patterns we
* strip off the already-matched parent segments so React Router receives the remainder.
*/
export const derivePathnameToMatch = (fullPathname: string, routePath?: string): string => {
if (!routePath || routePath === '' || routePath.startsWith('/')) {
return fullPathname;
}
const trimmedPath = fullPathname.startsWith('/') ? fullPathname.slice(1) : fullPathname;
if (!trimmedPath) {
return '';
}
const fullSegments = trimmedPath.split('/').filter(Boolean);
if (fullSegments.length === 0) {
return '';
}
const routeSegments = routePath.split('/').filter(Boolean);
if (routeSegments.length === 0) {
return trimmedPath;
}
const wildcardIndex = routeSegments.findIndex((segment) => segment === '*' || segment === '**');
if (wildcardIndex >= 0) {
const baseSegments = routeSegments.slice(0, wildcardIndex);
if (baseSegments.length === 0) {
return trimmedPath;
}
const startIndex = fullSegments.findIndex((_, idx) =>
baseSegments.every((seg, segIdx) => {
const target = fullSegments[idx + segIdx];
if (!target) {
return false;
}
if (seg.startsWith(':')) {
return true;
}
return target === seg;
})
);
if (startIndex >= 0) {
return fullSegments.slice(startIndex).join('/');
}
}
if (routeSegments.length <= fullSegments.length) {
return fullSegments.slice(fullSegments.length - routeSegments.length).join('/');
}
return fullSegments[fullSegments.length - 1] ?? trimmedPath;
};

View File

@@ -1,19 +0,0 @@
import React from 'react';
import { Routes } from 'react-router';
export const getRoutesChildren = (node: React.ReactNode) => {
// The use of `<Routes />` is encouraged with React Router v6.
let routesNode: React.ReactNode;
React.Children.forEach(node as React.ReactElement, (child: React.ReactElement) => {
if (child.type === Routes) {
routesNode = child;
}
});
if (routesNode) {
// The children of the `<Routes />` component are most likely
// (and should be) the `<Route />` components.
return (routesNode as React.ReactElement).props.children;
}
return undefined;
};

View File

@@ -1,6 +1,9 @@
import type { PathMatch } from 'react-router';
import { matchPath as reactRouterMatchPath } from 'react-router-dom';
/**
* Options for the matchPath function.
*/
interface MatchPathOptions {
/**
* The pathname to match against.
@@ -99,3 +102,61 @@ export const matchPath = ({ pathname, componentProps }: MatchPathOptions): PathM
return reactRouterMatchPath(matchOptions, pathname);
};
/**
* Determines the portion of a pathname that a given route pattern should match against.
* For absolute route patterns we return the full pathname. For relative patterns we
* strip off the already-matched parent segments so React Router receives the remainder.
*/
export const derivePathnameToMatch = (fullPathname: string, routePath?: string): string => {
if (!routePath || routePath === '' || routePath.startsWith('/')) {
return fullPathname;
}
const trimmedPath = fullPathname.startsWith('/') ? fullPathname.slice(1) : fullPathname;
if (!trimmedPath) {
return '';
}
const fullSegments = trimmedPath.split('/').filter(Boolean);
if (fullSegments.length === 0) {
return '';
}
const routeSegments = routePath.split('/').filter(Boolean);
if (routeSegments.length === 0) {
return trimmedPath;
}
const wildcardIndex = routeSegments.findIndex((segment) => segment === '*' || segment === '**');
if (wildcardIndex >= 0) {
const baseSegments = routeSegments.slice(0, wildcardIndex);
if (baseSegments.length === 0) {
return trimmedPath;
}
const startIndex = fullSegments.findIndex((_, idx) =>
baseSegments.every((seg, segIdx) => {
const target = fullSegments[idx + segIdx];
if (!target) {
return false;
}
if (seg.startsWith(':')) {
return true;
}
return target === seg;
})
);
if (startIndex >= 0) {
return fullSegments.slice(startIndex).join('/');
}
}
if (routeSegments.length <= fullSegments.length) {
return fullSegments.slice(fullSegments.length - routeSegments.length).join('/');
}
return fullSegments[fullSegments.length - 1] ?? trimmedPath;
};

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
/**
* Extracts the children from a Routes wrapper component.
* The use of `<Routes />` is encouraged with React Router v6.
*
* @param node The React node to extract Routes children from.
* @returns The children of the Routes component, or undefined if not found.
*/
export const getRoutesChildren = (node: React.ReactNode): React.ReactNode | undefined => {
let routesNode: React.ReactNode;
React.Children.forEach(node as React.ReactElement, (child: React.ReactElement) => {
if (child.type === Routes) {
routesNode = child;
}
});
if (routesNode) {
// The children of the `<Routes />` component are most likely
// (and should be) the `<Route />` components.
return (routesNode as React.ReactElement).props.children;
}
return undefined;
};
/**
* Extracts Route children from a node (either directly or from a Routes wrapper).
*
* @param children The children to extract routes from.
* @returns An array of Route elements.
*/
export const extractRouteChildren = (children: React.ReactNode): React.ReactElement[] => {
const routesChildren = getRoutesChildren(children) ?? children;
return React.Children.toArray(routesChildren).filter(
(child): child is React.ReactElement => React.isValidElement(child) && child.type === Route
);
};
/**
* Checks if a React element is a Navigate component (redirect).
*
* @param element The element to check.
* @returns True if the element is a Navigate component.
*/
export const isNavigateElement = (element: unknown): boolean => {
return (
React.isValidElement(element) &&
(element.type === Navigate || (typeof element.type === 'function' && element.type.name === 'Navigate'))
);
};

View File

@@ -1,22 +1,5 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import type { ViewItem } from '@ionic/react';
/**
* Checks if a React element is a Navigate component (redirect).
*
* @param element The element to check.
* @returns True if the element is a Navigate component.
*/
export const isNavigateElement = (element: unknown): boolean => {
return (
React.isValidElement(element) &&
(element.type === Navigate ||
(typeof element.type === 'function' && element.type.name === 'Navigate'))
);
};
/**
* Sorts view items by route specificity (most specific first).
* - Exact matches (no wildcards/params) come first