Skip to content

Commit 99dcb35

Browse files
committed
chore(react-router): minor clean up
1 parent 418ac75 commit 99dcb35

File tree

8 files changed

+143
-117
lines changed

8 files changed

+143
-117
lines changed

docs/react-router/react-router-6-status.md

Lines changed: 134 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22

33
**Branch:** `sk/react-router-6`
44
**Design Docs:** [PR #305](https://github.com/ionic-team/ionic-framework-design-documents/pull/305)
5-
**Last Updated:** November 26, 2025
5+
**Last Updated:** December 2, 2025
66

77
## Overview
88

99
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.
1010

11-
All Cypress tests are now passing.
11+
All Cypress tests are passing.
1212

1313
| Metric | Count |
1414
|--------|-------|
15-
| Total tests | 70 |
16-
| Passing | 70 |
15+
| Total tests | 77 |
16+
| Passing | 77 |
1717
| Failing | 0 |
1818

1919
## What Changed
@@ -29,31 +29,53 @@ The `@ionic/react-router` package now requires React Router 6:
2929
}
3030
```
3131

32+
The `history` package dependency was updated from v4 to v5 (which RR6 uses internally).
33+
3234
### Core Components
3335

34-
**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.
36+
**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.
3537

36-
**ReactRouterViewStack** was substantially expanded to handle RR6's matching semantics. Key additions include:
38+
**ReactRouterViewStack** (`ReactRouterViewStack.tsx`) was substantially expanded to handle RR6's matching semantics. Key changes include:
3739
- Support for RR6's `PathMatch` objects and pattern matching
3840
- Handling of index routes and wildcard routes (`*`)
3941
- View identity tracking for parameterized routes (`/user/:id`)
4042
- Proper computation of parent paths for nested outlets
43+
- View cleanup for cross-navigation scenarios (e.g., navigating between different tab stacks)
4144

42-
**StackManager** was updated to work with the new view stack implementation. Changes include:
45+
**StackManager** (`StackManager.tsx`) was updated to work with the new view stack implementation. Changes include:
4346
- Parent path derivation using RR6's route matching
4447
- Improved handling of `Navigate` redirect components
4548
- Better coordination of entering/leaving views during transitions
49+
- Hiding of deactivated catch-all routes to prevent visual glitches
50+
51+
**IonRouteInner** (`IonRouteInner.tsx`) was simplified to work with RR6's element-based routing instead of the component/render prop pattern.
52+
53+
**Router Components** (`IonReactRouter.tsx`, `IonReactHashRouter.tsx`, `IonReactMemoryRouter.tsx`) were updated to use RR6's router components and hooks.
4654

4755
### New Utilities
4856

49-
Four utility modules were added to support RR6's routing model:
57+
Five utility modules were added to support RR6's routing model:
5058

5159
| File | Purpose |
5260
|------|---------|
53-
| `matchPath.ts` | Extended path matching with RR6 pattern syntax |
54-
| `matchRoutesFromChildren.ts` | Converts Route children to RouteObjects for RR6's `matchRoutes` |
55-
| `derivePathnameToMatch.ts` | Computes the pathname portion relevant to a nested outlet |
56-
| `findRoutesNode.ts` | Locates Routes containers in the component tree |
61+
| `computeParentPath.ts` | Computes common path prefixes and determines specific route matches for nested outlets |
62+
| `pathMatching.ts` | Extended path matching using RR6's `matchPath` with support for index routes |
63+
| `pathNormalization.ts` | Path string normalization (leading/trailing slashes) |
64+
| `routeElements.ts` | Extracts Route children from Routes wrappers, detects Navigate elements |
65+
| `viewItemUtils.ts` | Sorts views by route specificity for proper matching priority |
66+
67+
The old `matchPath.ts` utility was removed as its functionality is now handled by RR6's native matching.
68+
69+
### @ionic/react Changes
70+
71+
Several changes were made to the `@ionic/react` package to support the migration:
72+
73+
- **IonRoute** (`IonRoute.tsx`): Updated to work with RR6's element-based routing
74+
- **IonRouterOutlet** (`IonRouterOutlet.tsx`): Updated for RR6 compatibility
75+
- **LocationHistory** (`LocationHistory.ts`): Enhanced to track navigation direction more accurately
76+
- **RouteManagerContext** (`RouteManagerContext.ts`): Added `clearOutletViews` method for cross-navigation cleanup
77+
- **ViewLifeCycleManager** (`ViewLifeCycleManager.tsx`): Added support for new lifecycle events
78+
- **createInlineOverlayComponent** (`createInlineOverlayComponent.tsx`): New utility to automatically dismiss inline overlays on navigation
5779

5880
### Test App Updates
5981

@@ -62,6 +84,11 @@ All test pages in `packages/react-router/test/base/src/pages/` were updated to u
6284
- Nested routes now require trailing wildcards (`path="parent/*"`) when they contain child outlets
6385
- `<Redirect to="..." />` became `<Navigate to="..." replace />`
6486
- Route params accessed via `useParams()` instead of `props.match.params`
87+
- Links use relative paths where appropriate
88+
89+
A new test page (`nested-params/NestedParams.tsx`) was added to test parameterized nested routing scenarios.
90+
91+
The `reactrouter5` test app was removed since the package no longer supports RR5.
6592

6693
## Test Coverage
6794

@@ -72,17 +99,26 @@ The Cypress test suite covers the following scenarios:
7299
| routing.cy.js | 29 | Core navigation, tabs, back button, redirects, params |
73100
| nested-outlets.cy.js | 11 | Nested `IonRouterOutlet` behavior, back navigation |
74101
| swipe-to-go-back.cy.js | 8 | Gesture navigation, abort handling, tab interactions |
102+
| cross-route-navigation.cy.js | 7 | Navigation between different route contexts (tabs, outlets) |
75103
| multiple-tabs.cy.js | 4 | Switching between different tab configurations |
76-
| dynamic-tabs.cy.js | 3 | Adding tabs at runtime |
77-
| dynamic-routes.cy.js | 3 | Adding routes at runtime |
78104
| overlays.cy.js | 3 | Modal cleanup on navigation |
79-
| refs.cy.js | 2 | Ref forwarding to Ionic components |
105+
| dynamic-routes.cy.js | 3 | Adding routes at runtime |
106+
| dynamic-tabs.cy.js | 3 | Adding tabs at runtime |
80107
| tabs.cy.js | 2 | Basic tab navigation and history |
81108
| tab-context.cy.js | 2 | Programmatic tab switching via context |
109+
| refs.cy.js | 2 | Ref forwarding to Ionic components |
82110
| dynamic-ionpage-classnames.cy.js | 1 | Dynamic class application to IonPage |
83111
| outlet-ref.cy.js | 1 | Ref access to IonRouterOutlet |
84112
| replace-actions.cy.js | 1 | History replacement behavior |
85113

114+
### New Test Suite: Cross-Route Navigation
115+
116+
A new test suite (`cross-route-navigation.cy.js`) was added to verify proper view cleanup when navigating between different route contexts:
117+
- Tab-to-non-tab navigation
118+
- Non-tab-to-tab navigation
119+
- Between different tab configurations
120+
- Deep link scenarios
121+
86122
## Known Limitations
87123

88124
### Route Path Syntax
@@ -99,74 +135,118 @@ Nested outlets require parent routes to include a trailing wildcard:
99135

100136
This aligns with React Router 6's nested routing semantics where child routes are matched relative to the parent's path.
101137

102-
## Next Steps
138+
### Relative vs Absolute Paths
103139

104-
### Before Alpha Release
105-
106-
1. ~~**Run a TypeScript strict check** on the `@ionic/react-router` package~~ Complete - all type errors resolved
107-
2. **Manual testing pass** through the test app to verify animations and gestures feel correct
108-
109-
### Documentation
140+
React Router 6 strongly favors relative paths for nested routing. While absolute paths still work, using relative paths in nested outlets is recommended:
110141

111-
The following documentation should be prepared before public release:
112-
113-
1. **Migration guide** covering:
114-
- Route syntax changes (`component` to `element`, `Redirect` to `Navigate`)
115-
- Nested route wildcard requirements
116-
- Accessing route params with hooks
117-
- Any removed or deprecated APIs
118-
119-
2. **Updated API reference** for:
120-
- `IonReactRouter`, `IonReactHashRouter`, `IonReactMemoryRouter`
121-
- `IonRouterOutlet` behavior with nested routes
122-
- `routeOptions.unmount` and `LocationHistory` behavior
142+
```tsx
143+
// Inside a component at /tabs/home
144+
<Link to="details">Details</Link> // Navigates to /tabs/home/details
145+
<Link to="/tabs/home/details">Details</Link> // Also works, but less flexible
146+
```
123147

124-
### CI Integration
148+
## CI Integration
125149

126150
The GitHub Actions workflows have been updated to run the RR6 test app:
127151
- `.github/workflows/build.yml`
128152
- `.github/workflows/stencil-nightly.yml`
129153

154+
The matrix now uses `reactrouter6` instead of `reactrouter5`.
155+
130156
## Architecture Reference
131157

132158
The data flow through the routing system:
133159

134160
```
135161
Browser History Change
136-
137-
162+
|
163+
v
138164
IonRouter (useLocation/useNavigate)
139-
140-
├── Updates LocationHistory
141-
├── Computes RouteInfo (action, direction, params)
142-
143-
165+
|
166+
+-- Updates LocationHistory
167+
+-- Computes RouteInfo (action, direction, params)
168+
|
169+
v
144170
RouteManagerContext
145-
146-
171+
|
172+
v
147173
StackManager (per IonRouterOutlet)
148-
149-
├── Derives parent path from route children
150-
├── Matches routes using ReactRouterViewStack
151-
├── Determines entering/leaving views
152-
153-
174+
|
175+
+-- Derives parent path from route children
176+
+-- Matches routes using ReactRouterViewStack
177+
+-- Determines entering/leaving views
178+
+-- Clears stale views on cross-navigation
179+
|
180+
v
154181
ion-router-outlet.commit()
155-
156-
182+
|
183+
v
157184
Native Ionic Transition
158185
```
159186

160187
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.
161188

189+
## Next Steps
190+
191+
### Before Release
192+
193+
1. ~~**Run a TypeScript strict check** on the `@ionic/react-router` package~~ Complete
194+
2. ~~**All Cypress tests passing**~~ Complete (77/77)
195+
3. **Manual testing pass** through the test app to verify animations and gestures feel correct
196+
4. **Code review** of the implementation
197+
198+
### Documentation
199+
200+
The following documentation should be prepared before public release:
201+
202+
1. **Migration guide** covering:
203+
- Route syntax changes (`component` to `element`, `Redirect` to `Navigate`)
204+
- Nested route wildcard requirements
205+
- Accessing route params with hooks (`useParams()`)
206+
- Link syntax changes (relative paths)
207+
- Any removed or deprecated APIs
208+
209+
2. **Updated API reference** for:
210+
- `IonReactRouter`, `IonReactHashRouter`, `IonReactMemoryRouter`
211+
- `IonRouterOutlet` behavior with nested routes
212+
- `routeOptions.unmount` and `LocationHistory` behavior
213+
162214
## Related Files
163215

164216
Source code:
165217
- `packages/react-router/src/ReactRouter/IonRouter.tsx`
166218
- `packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx`
167219
- `packages/react-router/src/ReactRouter/StackManager.tsx`
168-
- `packages/react-router/src/ReactRouter/utils/`
220+
- `packages/react-router/src/ReactRouter/IonRouteInner.tsx`
221+
- `packages/react-router/src/ReactRouter/IonReactRouter.tsx`
222+
- `packages/react-router/src/ReactRouter/IonReactHashRouter.tsx`
223+
- `packages/react-router/src/ReactRouter/IonReactMemoryRouter.tsx`
224+
- `packages/react-router/src/ReactRouter/utils/` (5 utility modules)
169225

170-
Test app:
226+
Test infrastructure:
171227
- `packages/react-router/test/base/` (shared test code)
172228
- `packages/react-router/test/apps/reactrouter6/` (RR6 config)
229+
- `packages/react-router/scripts/test_runner.sh` (test automation)
230+
231+
Supporting changes in @ionic/react:
232+
- `packages/react/src/components/IonRoute.tsx`
233+
- `packages/react/src/components/IonRouterOutlet.tsx`
234+
- `packages/react/src/components/createInlineOverlayComponent.tsx`
235+
- `packages/react/src/routing/LocationHistory.ts`
236+
- `packages/react/src/routing/RouteManagerContext.ts`
237+
- `packages/react/src/routing/ViewLifeCycleManager.tsx`
238+
239+
## Commit History
240+
241+
Key commits on this branch:
242+
243+
| Commit | Description |
244+
|--------|-------------|
245+
| `418ac75` | Cleaning up util files |
246+
| `82fd1ba` | Fix views not being cleaned up properly, causing cross navigation issues |
247+
| `3912623` | Hide deactivated catch-all routes |
248+
| `7fd0659` | Nested redirect fix |
249+
| `b02c197` | Prevent incorrect view reuse for parameterized routes |
250+
| `584dcf2` | Prioritize specific route matches |
251+
| `045b0a7` | Correct tab and nested outlet navigation |
252+
| `59f2dbe` | Automatically dismiss inline overlays on navigation |

packages/react-router/src/ReactRouter/IonRouter.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* and animate.
77
*/
88

9-
import type { AnimationBuilder, RouteAction, RouteInfo, RouteManagerContextState, RouterDirection } from '@ionic/react';
9+
import type { AnimationBuilder, RouteAction, RouteInfo, RouteManagerContextState, RouterDirection, RouterOptions } from '@ionic/react';
1010
import { LocationHistory, NavManager, RouteManagerContext, generateId, getConfig } from '@ionic/react';
1111
import type { Action as HistoryAction, Location } from 'history';
1212
import type { PropsWithChildren } from 'react';
@@ -22,7 +22,7 @@ type HistoryLocation = Location;
2222

2323
export interface LocationState {
2424
direction?: RouterDirection;
25-
routerOptions?: { as?: string; unmount?: boolean };
25+
routerOptions?: RouterOptions;
2626
}
2727

2828
interface IonRouterProps {

packages/react-router/test/base/src/pages/routing/Tab1.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ const Tab1: React.FC = () => {
5454
<IonItem routerLink="/routing/tabs/home/details/1">
5555
<IonLabel>Details 1</IonLabel>
5656
</IonItem>
57-
<IonItem routerLink="/routing/tabs/home/details/1" routerOptions={{ unmount: true }}>
58-
<IonLabel>Details 1 & Unmount</IonLabel>
57+
<IonItem routerLink="/routing/tabs/home/details/1">
58+
<IonLabel>Details 1 (alt)</IonLabel>
5959
</IonItem>
6060
<IonItem routerLink="/routing/tabs/home/details/1?hello=there">
6161
<IonLabel>Details 1 with Query Params</IonLabel>

packages/react-router/test/base/src/pages/routing/Tabs.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ const Tabs: React.FC = () => {
3434
/>
3535
</IonRouterOutlet>
3636
<IonTabBar slot="bottom">
37-
<IonTabButton tab="home" href="/routing/tabs/home" routerOptions={{ unmount: true }}>
37+
<IonTabButton tab="home" href="/routing/tabs/home">
3838
<IonIcon icon={triangle} />
3939
<IonLabel>Home</IonLabel>
4040
</IonTabButton>
41-
<IonTabButton tab="settings" href="/routing/tabs/settings" routerOptions={{ unmount: true }}>
41+
<IonTabButton tab="settings" href="/routing/tabs/settings">
4242
<IonIcon icon={ellipse} />
4343
<IonLabel>Settings</IonLabel>
4444
</IonTabButton>
45-
<IonTabButton tab="tab3" href="/routing/tabs/tab3" routerOptions={{ unmount: true }}>
45+
<IonTabButton tab="tab3" href="/routing/tabs/tab3">
4646
<IonIcon icon={square} />
4747
<IonLabel>Tab 3</IonLabel>
4848
</IonTabButton>

packages/react-router/test/base/src/pages/tab-context/TabContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const TabsContext: React.FC = () => {
2828
<Route path="tab2" element={<Tab2 />} />
2929
</IonRouterOutlet>
3030
<IonTabBar slot="bottom">
31-
<IonTabButton tab="tab1" href="/tab-context/tab1" routerOptions={{ unmount: true }}>
31+
<IonTabButton tab="tab1" href="/tab-context/tab1">
3232
<IonIcon icon={triangle} />
3333
<IonLabel>Tab1</IonLabel>
3434
</IonTabButton>

packages/react-router/test/base/src/utils/LocationHistory.ts

Lines changed: 0 additions & 45 deletions
This file was deleted.

packages/react/src/components/createInlineOverlayComponent.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,7 @@ export const createInlineOverlayComponent = <PropType, ElementType>(
8686
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
8787
const target = mutation.target as HTMLElement;
8888
// If any element gets the ion-page-hidden or ion-page-invisible class, dismiss overlay
89-
if (
90-
target.classList.contains('ion-page-hidden') ||
91-
target.classList.contains('ion-page-invisible')
92-
) {
89+
if (target.classList.contains('ion-page-hidden') || target.classList.contains('ion-page-invisible')) {
9390
this.dismissOverlay();
9491
return;
9592
}

0 commit comments

Comments
 (0)