diff --git a/docs/react-router/react-router-6-status.md b/docs/react-router/react-router-6-status.md
index 30aeb65a83..5488c79967 100644
--- a/docs/react-router/react-router-6-status.md
+++ b/docs/react-router/react-router-6-status.md
@@ -2,18 +2,18 @@
**Branch:** `sk/react-router-6`
**Design Docs:** [PR #305](https://github.com/ionic-team/ionic-framework-design-documents/pull/305)
-**Last Updated:** November 26, 2025
+**Last Updated:** December 2, 2025
## Overview
The `@ionic/react-router` package has been updated to support React Router 6. This migration replaces the React Router 5 integration with native RR6 APIs while preserving Ionic's navigation patterns, animations, and view lifecycle management.
-All Cypress tests are now passing.
+All Cypress tests are passing.
| Metric | Count |
|--------|-------|
-| Total tests | 70 |
-| Passing | 70 |
+| Total tests | 77 |
+| Passing | 77 |
| Failing | 0 |
## What Changed
@@ -29,31 +29,53 @@ The `@ionic/react-router` package now requires React Router 6:
}
```
+The `history` package dependency was updated from v4 to v5 (which RR6 uses internally).
+
### Core Components
-**IonRouter** was rewritten as a functional component using React hooks. It now uses `useLocation` and `useNavigate` from React Router 6 instead of the `withRouter` HOC and `history` object from v5. The component continues to manage `LocationHistory` and compute `RouteInfo` objects for Ionic's view stacks and transition directions.
+**IonRouter** (`IonRouter.tsx`) was rewritten as a functional component using React hooks. It now uses `useLocation` and `useNavigate` from React Router 6 instead of the `withRouter` HOC and `history` object from v5. The component continues to manage `LocationHistory` and compute `RouteInfo` objects for Ionic's view stacks and transition directions.
-**ReactRouterViewStack** was substantially expanded to handle RR6's matching semantics. Key additions include:
+**ReactRouterViewStack** (`ReactRouterViewStack.tsx`) was substantially expanded to handle RR6's matching semantics. Key changes include:
- Support for RR6's `PathMatch` objects and pattern matching
- Handling of index routes and wildcard routes (`*`)
- View identity tracking for parameterized routes (`/user/:id`)
- Proper computation of parent paths for nested outlets
+- View cleanup for cross-navigation scenarios (e.g., navigating between different tab stacks)
-**StackManager** was updated to work with the new view stack implementation. Changes include:
+**StackManager** (`StackManager.tsx`) was updated to work with the new view stack implementation. Changes include:
- Parent path derivation using RR6's route matching
- Improved handling of `Navigate` redirect components
- Better coordination of entering/leaving views during transitions
+- Hiding of deactivated catch-all routes to prevent visual glitches
+
+**IonRouteInner** (`IonRouteInner.tsx`) was simplified to work with RR6's element-based routing instead of the component/render prop pattern.
+
+**Router Components** (`IonReactRouter.tsx`, `IonReactHashRouter.tsx`, `IonReactMemoryRouter.tsx`) were updated to use RR6's router components and hooks.
### New Utilities
-Four utility modules were added to support RR6's routing model:
+Five utility modules were added to support RR6's routing model:
| File | Purpose |
|------|---------|
-| `matchPath.ts` | Extended path matching with RR6 pattern syntax |
-| `matchRoutesFromChildren.ts` | Converts Route children to RouteObjects for RR6's `matchRoutes` |
-| `derivePathnameToMatch.ts` | Computes the pathname portion relevant to a nested outlet |
-| `findRoutesNode.ts` | Locates Routes containers in the component tree |
+| `computeParentPath.ts` | Computes common path prefixes and determines specific route matches for nested outlets |
+| `pathMatching.ts` | Extended path matching using RR6's `matchPath` with support for index routes |
+| `pathNormalization.ts` | Path string normalization (leading/trailing slashes) |
+| `routeElements.ts` | Extracts Route children from Routes wrappers, detects Navigate elements |
+| `viewItemUtils.ts` | Sorts views by route specificity for proper matching priority |
+
+The old `matchPath.ts` utility was removed as its functionality is now handled by RR6's native matching.
+
+### @ionic/react Changes
+
+Several changes were made to the `@ionic/react` package to support the migration:
+
+- **IonRoute** (`IonRoute.tsx`): Updated to work with RR6's element-based routing
+- **IonRouterOutlet** (`IonRouterOutlet.tsx`): Updated for RR6 compatibility
+- **LocationHistory** (`LocationHistory.ts`): Enhanced to track navigation direction more accurately
+- **RouteManagerContext** (`RouteManagerContext.ts`): Added `clearOutletViews` method for cross-navigation cleanup
+- **ViewLifeCycleManager** (`ViewLifeCycleManager.tsx`): Added support for new lifecycle events
+- **createInlineOverlayComponent** (`createInlineOverlayComponent.tsx`): New utility to automatically dismiss inline overlays on navigation
### Test App Updates
@@ -62,6 +84,11 @@ All test pages in `packages/react-router/test/base/src/pages/` were updated to u
- Nested routes now require trailing wildcards (`path="parent/*"`) when they contain child outlets
- `` became ``
- Route params accessed via `useParams()` instead of `props.match.params`
+- Links use relative paths where appropriate
+
+A new test page (`nested-params/NestedParams.tsx`) was added to test parameterized nested routing scenarios.
+
+The `reactrouter5` test app was removed since the package no longer supports RR5.
## Test Coverage
@@ -72,17 +99,26 @@ The Cypress test suite covers the following scenarios:
| routing.cy.js | 29 | Core navigation, tabs, back button, redirects, params |
| nested-outlets.cy.js | 11 | Nested `IonRouterOutlet` behavior, back navigation |
| swipe-to-go-back.cy.js | 8 | Gesture navigation, abort handling, tab interactions |
+| cross-route-navigation.cy.js | 7 | Navigation between different route contexts (tabs, outlets) |
| multiple-tabs.cy.js | 4 | Switching between different tab configurations |
-| dynamic-tabs.cy.js | 3 | Adding tabs at runtime |
-| dynamic-routes.cy.js | 3 | Adding routes at runtime |
| overlays.cy.js | 3 | Modal cleanup on navigation |
-| refs.cy.js | 2 | Ref forwarding to Ionic components |
+| dynamic-routes.cy.js | 3 | Adding routes at runtime |
+| dynamic-tabs.cy.js | 3 | Adding tabs at runtime |
| tabs.cy.js | 2 | Basic tab navigation and history |
| tab-context.cy.js | 2 | Programmatic tab switching via context |
+| refs.cy.js | 2 | Ref forwarding to Ionic components |
| dynamic-ionpage-classnames.cy.js | 1 | Dynamic class application to IonPage |
| outlet-ref.cy.js | 1 | Ref access to IonRouterOutlet |
| replace-actions.cy.js | 1 | History replacement behavior |
+### New Test Suite: Cross-Route Navigation
+
+A new test suite (`cross-route-navigation.cy.js`) was added to verify proper view cleanup when navigating between different route contexts:
+- Tab-to-non-tab navigation
+- Non-tab-to-tab navigation
+- Between different tab configurations
+- Deep link scenarios
+
## Known Limitations
### Route Path Syntax
@@ -99,12 +135,65 @@ Nested outlets require parent routes to include a trailing wildcard:
This aligns with React Router 6's nested routing semantics where child routes are matched relative to the parent's path.
+### Relative vs Absolute Paths
+
+React Router 6 strongly favors relative paths for nested routing. While absolute paths still work, using relative paths in nested outlets is recommended:
+
+```tsx
+// Inside a component at /tabs/home
+Details // Navigates to /tabs/home/details
+Details // Also works, but less flexible
+```
+
+## CI Integration
+
+The GitHub Actions workflows have been updated to run the RR6 test app:
+- `.github/workflows/build.yml`
+- `.github/workflows/stencil-nightly.yml`
+
+The matrix now uses `reactrouter6` instead of `reactrouter5`.
+
+## Architecture Reference
+
+The data flow through the routing system:
+
+```
+Browser History Change
+ |
+ v
+ IonRouter (useLocation/useNavigate)
+ |
+ +-- Updates LocationHistory
+ +-- Computes RouteInfo (action, direction, params)
+ |
+ v
+ RouteManagerContext
+ |
+ v
+ StackManager (per IonRouterOutlet)
+ |
+ +-- Derives parent path from route children
+ +-- Matches routes using ReactRouterViewStack
+ +-- Determines entering/leaving views
+ +-- Clears stale views on cross-navigation
+ |
+ v
+ ion-router-outlet.commit()
+ |
+ v
+ Native Ionic Transition
+```
+
+The key insight is that Ionic intercepts React Router's navigation events and translates them into its own view management system, which enables native-feeling animations and gestures while still using React Router for URL management.
+
## Next Steps
-### Before Alpha Release
+### Before Release
-1. ~~**Run a TypeScript strict check** on the `@ionic/react-router` package~~ Complete - all type errors resolved
-2. **Manual testing pass** through the test app to verify animations and gestures feel correct
+1. ~~**Run a TypeScript strict check** on the `@ionic/react-router` package~~ Complete
+2. ~~**All Cypress tests passing**~~ Complete (77/77)
+3. **Manual testing pass** through the test app to verify animations and gestures feel correct
+4. **Code review** of the implementation
### Documentation
@@ -113,7 +202,8 @@ The following documentation should be prepared before public release:
1. **Migration guide** covering:
- Route syntax changes (`component` to `element`, `Redirect` to `Navigate`)
- Nested route wildcard requirements
- - Accessing route params with hooks
+ - Accessing route params with hooks (`useParams()`)
+ - Link syntax changes (relative paths)
- Any removed or deprecated APIs
2. **Updated API reference** for:
@@ -121,52 +211,42 @@ The following documentation should be prepared before public release:
- `IonRouterOutlet` behavior with nested routes
- `routeOptions.unmount` and `LocationHistory` behavior
-### CI Integration
-
-The GitHub Actions workflows have been updated to run the RR6 test app:
-- `.github/workflows/build.yml`
-- `.github/workflows/stencil-nightly.yml`
-
-## Architecture Reference
-
-The data flow through the routing system:
-
-```
-Browser History Change
- │
- ▼
- IonRouter (useLocation/useNavigate)
- │
- ├── Updates LocationHistory
- ├── Computes RouteInfo (action, direction, params)
- │
- ▼
- RouteManagerContext
- │
- ▼
- StackManager (per IonRouterOutlet)
- │
- ├── Derives parent path from route children
- ├── Matches routes using ReactRouterViewStack
- ├── Determines entering/leaving views
- │
- ▼
- ion-router-outlet.commit()
- │
- ▼
- Native Ionic Transition
-```
-
-The key insight is that Ionic intercepts React Router's navigation events and translates them into its own view management system, which enables native-feeling animations and gestures while still using React Router for URL management.
-
## Related Files
Source code:
- `packages/react-router/src/ReactRouter/IonRouter.tsx`
- `packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx`
- `packages/react-router/src/ReactRouter/StackManager.tsx`
-- `packages/react-router/src/ReactRouter/utils/`
+- `packages/react-router/src/ReactRouter/IonRouteInner.tsx`
+- `packages/react-router/src/ReactRouter/IonReactRouter.tsx`
+- `packages/react-router/src/ReactRouter/IonReactHashRouter.tsx`
+- `packages/react-router/src/ReactRouter/IonReactMemoryRouter.tsx`
+- `packages/react-router/src/ReactRouter/utils/` (5 utility modules)
-Test app:
+Test infrastructure:
- `packages/react-router/test/base/` (shared test code)
- `packages/react-router/test/apps/reactrouter6/` (RR6 config)
+- `packages/react-router/scripts/test_runner.sh` (test automation)
+
+Supporting changes in @ionic/react:
+- `packages/react/src/components/IonRoute.tsx`
+- `packages/react/src/components/IonRouterOutlet.tsx`
+- `packages/react/src/components/createInlineOverlayComponent.tsx`
+- `packages/react/src/routing/LocationHistory.ts`
+- `packages/react/src/routing/RouteManagerContext.ts`
+- `packages/react/src/routing/ViewLifeCycleManager.tsx`
+
+## Commit History
+
+Key commits on this branch:
+
+| Commit | Description |
+|--------|-------------|
+| `418ac75` | Cleaning up util files |
+| `82fd1ba` | Fix views not being cleaned up properly, causing cross navigation issues |
+| `3912623` | Hide deactivated catch-all routes |
+| `7fd0659` | Nested redirect fix |
+| `b02c197` | Prevent incorrect view reuse for parameterized routes |
+| `584dcf2` | Prioritize specific route matches |
+| `045b0a7` | Correct tab and nested outlet navigation |
+| `59f2dbe` | Automatically dismiss inline overlays on navigation |
diff --git a/packages/react-router/src/ReactRouter/IonRouter.tsx b/packages/react-router/src/ReactRouter/IonRouter.tsx
index ae073bc39d..18700006aa 100644
--- a/packages/react-router/src/ReactRouter/IonRouter.tsx
+++ b/packages/react-router/src/ReactRouter/IonRouter.tsx
@@ -6,7 +6,7 @@
* and animate.
*/
-import type { AnimationBuilder, RouteAction, RouteInfo, RouteManagerContextState, RouterDirection } from '@ionic/react';
+import type { AnimationBuilder, RouteAction, RouteInfo, RouteManagerContextState, RouterDirection, RouterOptions } from '@ionic/react';
import { LocationHistory, NavManager, RouteManagerContext, generateId, getConfig } from '@ionic/react';
import type { Action as HistoryAction, Location } from 'history';
import type { PropsWithChildren } from 'react';
@@ -22,7 +22,7 @@ type HistoryLocation = Location;
export interface LocationState {
direction?: RouterDirection;
- routerOptions?: { as?: string; unmount?: boolean };
+ routerOptions?: RouterOptions;
}
interface IonRouterProps {
diff --git a/packages/react-router/test/base/src/pages/routing/Tab1.tsx b/packages/react-router/test/base/src/pages/routing/Tab1.tsx
index 412a9e876a..d9e204d55a 100644
--- a/packages/react-router/test/base/src/pages/routing/Tab1.tsx
+++ b/packages/react-router/test/base/src/pages/routing/Tab1.tsx
@@ -54,8 +54,8 @@ const Tab1: React.FC = () => {
Details 1
-
- Details 1 & Unmount
+
+ Details 1 (alt)
Details 1 with Query Params
diff --git a/packages/react-router/test/base/src/pages/routing/Tabs.tsx b/packages/react-router/test/base/src/pages/routing/Tabs.tsx
index 819fa7f16b..d70ffd7caa 100644
--- a/packages/react-router/test/base/src/pages/routing/Tabs.tsx
+++ b/packages/react-router/test/base/src/pages/routing/Tabs.tsx
@@ -34,15 +34,15 @@ const Tabs: React.FC = () => {
/>
-
+
Home
-
+
Settings
-
+
Tab 3
diff --git a/packages/react-router/test/base/src/pages/tab-context/TabContext.tsx b/packages/react-router/test/base/src/pages/tab-context/TabContext.tsx
index bd2ca28b27..e200f1e811 100644
--- a/packages/react-router/test/base/src/pages/tab-context/TabContext.tsx
+++ b/packages/react-router/test/base/src/pages/tab-context/TabContext.tsx
@@ -28,7 +28,7 @@ const TabsContext: React.FC = () => {
} />
-
+
Tab1
diff --git a/packages/react-router/test/base/src/utils/LocationHistory.ts b/packages/react-router/test/base/src/utils/LocationHistory.ts
deleted file mode 100644
index 50faca088a..0000000000
--- a/packages/react-router/test/base/src/utils/LocationHistory.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import type { Location as HistoryLocation } from 'history';
-
-const RESTRICT_SIZE = 25;
-
-export class LocationHistory {
- private locationHistory: HistoryLocation[] = [];
-
- add(location: HistoryLocation) {
- this.locationHistory.push(location);
- if (this.locationHistory.length > RESTRICT_SIZE) {
- this.locationHistory.splice(0, 10);
- }
- }
-
- pop() {
- this.locationHistory.pop();
- }
-
- replace(location: HistoryLocation) {
- this.locationHistory.pop();
- this.locationHistory.push(location);
- }
-
- clear() {
- this.locationHistory = [];
- }
-
- findLastLocationByUrl(url: string) {
- for (let i = this.locationHistory.length - 1; i >= 0; i--) {
- const location = this.locationHistory[i];
- if (location.pathname.toLocaleLowerCase() === url.toLocaleLowerCase()) {
- return location;
- }
- }
- return undefined;
- }
-
- previous() {
- return this.locationHistory[this.locationHistory.length - 2];
- }
-
- current() {
- return this.locationHistory[this.locationHistory.length - 1];
- }
-}
diff --git a/packages/react/src/components/createInlineOverlayComponent.tsx b/packages/react/src/components/createInlineOverlayComponent.tsx
index f359e136b2..26f6a9ac5e 100644
--- a/packages/react/src/components/createInlineOverlayComponent.tsx
+++ b/packages/react/src/components/createInlineOverlayComponent.tsx
@@ -86,10 +86,7 @@ export const createInlineOverlayComponent = (
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const target = mutation.target as HTMLElement;
// If any element gets the ion-page-hidden or ion-page-invisible class, dismiss overlay
- if (
- target.classList.contains('ion-page-hidden') ||
- target.classList.contains('ion-page-invisible')
- ) {
+ if (target.classList.contains('ion-page-hidden') || target.classList.contains('ion-page-invisible')) {
this.dismissOverlay();
return;
}
diff --git a/packages/react/src/routing/ViewLifeCycleManager.tsx b/packages/react/src/routing/ViewLifeCycleManager.tsx
index a546bcbd05..78b83f295a 100644
--- a/packages/react/src/routing/ViewLifeCycleManager.tsx
+++ b/packages/react/src/routing/ViewLifeCycleManager.tsx
@@ -40,12 +40,6 @@ export class ViewLifeCycleManager extends React.Component