|
1 | | -# React Router 6 Status Report |
| 1 | +# React Router 6 Migration |
2 | 2 |
|
3 | 3 | **Branch:** `sk/react-router-6` |
4 | 4 | **Design Docs:** [PR #305](https://github.com/ionic-team/ionic-framework-design-documents/pull/305) |
5 | | -**Last Updated:** November 22, 2025 |
| 5 | +**Last Updated:** November 26, 2025 |
6 | 6 |
|
7 | | -## Summary |
| 7 | +## Overview |
8 | 8 |
|
9 | | -The React Router 6 work is in good shape. The core router integration matches the design docs, the test app exercises most of the interesting cases, and the latest Cypress run shows that almost everything behaves as expected. What remains are a small set of advanced flows around not-found handling, some tab context behavior, and one swipe-to-go-back scenario. |
| 9 | +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. |
10 | 10 |
|
11 | | -At this point the work feels like late-stage refinement rather than architectural surgery. |
| 11 | +All Cypress tests are now passing. |
12 | 12 |
|
13 | | -- Total Cypress tests: 69 |
14 | | -- Passing: 62 |
15 | | -- Failing: 7 |
16 | | -- Suites with failures: `routing.cy.js`, `swipe-to-go-back.cy.js`, `tab-context.cy.js` |
| 13 | +| Metric | Count | |
| 14 | +|--------|-------| |
| 15 | +| Total tests | 70 | |
| 16 | +| Passing | 70 | |
| 17 | +| Failing | 0 | |
17 | 18 |
|
18 | | -Once those are cleaned up and the types/docs are brought up to date, this should be ready for an alpha release. |
| 19 | +## What Changed |
19 | 20 |
|
20 | | ---- |
| 21 | +### Package Dependencies |
21 | 22 |
|
22 | | -## Test status (Cypress – React Router 6 test app) |
| 23 | +The `@ionic/react-router` package now requires React Router 6: |
23 | 24 |
|
24 | | -Latest run of `npm run cypress` against the RR6 test app: |
25 | | - |
26 | | -- `dynamic-ionpage-classnames.cy.js` – 1/1 passing |
27 | | -- `dynamic-routes.cy.js` – 3/3 passing |
28 | | -- `dynamic-tabs.cy.js` – 3/3 passing |
29 | | -- `multiple-tabs.cy.js` – 4/4 passing |
30 | | -- `nested-outlets.cy.js` – 11/11 passing |
31 | | -- `outlet-ref.cy.js` – 1/1 passing |
32 | | -- `overlays.cy.js` – 3/3 passing |
33 | | -- `refs.cy.js` – 2/2 passing |
34 | | -- `replace-actions.cy.js` – 1/1 passing |
35 | | -- `routing.cy.js` – 24/28 passing (4 failing) |
36 | | -- `swipe-to-go-back.cy.js` – 7/8 passing (1 failing) |
37 | | -- `tab-context.cy.js` – 0/2 passing (2 failing) |
38 | | -- `tabs.cy.js` – 2/2 passing |
39 | | - |
40 | | -So the bulk of the suite is green. The remaining problems are concentrated in three areas: |
41 | | - |
42 | | -1. A handful of routing edge cases in `routing.cy.js`. |
43 | | -2. One swipe-back path that gets confused after tab switching. |
44 | | -3. The small tab-context demo that drives tabs via `IonTabsContext`. |
45 | | - |
46 | | ---- |
47 | | - |
48 | | -## What’s working well |
49 | | - |
50 | | -This is the “you don’t need to worry about these anymore” list. |
51 | | - |
52 | | -### Core architecture |
53 | | - |
54 | | -The main pieces match the RR6 design docs and behave sensibly in practice: |
55 | | - |
56 | | -- **`IonRouter`** uses `useLocation` / `useNavigate` from React Router 6, tracks a `LocationHistory`, and computes `RouteInfo` objects that drive Ionic’s view stacks and directions. |
57 | | -- **`ReactRouterViewStack`** owns view items per outlet, handles RR6-style matching (including params and wildcards), and coordinates mount/unmount via `ViewLifeCycleManager`. |
58 | | -- **`StackManager`** glues a given `ion-router-outlet` to the view stack. It: |
59 | | - - picks entering/leaving views, |
60 | | - - calls `routerOutlet.commit(...)` for transitions, |
61 | | - - handles nested outlets via a computed parent path, and |
62 | | - - wires in swipe-to-go-back. |
63 | | -- **Router variants** (`IonReactRouter`, `IonReactHashRouter`, `IonReactMemoryRouter`) all run on top of this stack and are behaving as expected. |
64 | | - |
65 | | -There are no obvious architectural gaps left; the remaining bugs are about particular flows using this machinery, not about missing pieces. |
66 | | - |
67 | | -### Navigation scenarios covered by passing suites |
68 | | - |
69 | | -All of these are currently green in Cypress and line up with the v5 behavior: |
70 | | - |
71 | | -- **Dynamic routes** – adding/removing routes at runtime and hitting them directly. |
72 | | -- **Dynamic tabs** – adding a second tab at runtime and keeping the view state correct. |
73 | | -- **Multiple tab bars** – apps that switch between different tab sets. |
74 | | -- **Nested outlets** – both the simple nested-outlet demo and the deeper `nested-outlet2` flows now pass end‑to‑end. |
75 | | -- **Overlays** – modal navigation (including navigating away while a modal is open) cleans up correctly. |
76 | | -- **Refs** – forwarding refs to Ionic components works for both function and class components. |
77 | | -- **Replace actions** – replace-style navigation behaves as expected in the dedicated demo. |
78 | | -- **Tabs** – the basic tabs test suite (separate from the tab-context demo) is fully passing. |
| 25 | +```json |
| 26 | +"peerDependencies": { |
| 27 | + "react-router": ">=6.0.0", |
| 28 | + "react-router-dom": ">=6.0.0" |
| 29 | +} |
| 30 | +``` |
79 | 31 |
|
80 | | -Taken together, that covers the majority of patterns people actually use: plain stacks, nested outlets, “tabs within a shell route”, and overlays. |
| 32 | +### Core Components |
81 | 33 |
|
82 | | ---- |
| 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. |
83 | 35 |
|
84 | | -## Remaining problem areas |
| 36 | +**ReactRouterViewStack** was substantially expanded to handle RR6's matching semantics. Key additions include: |
| 37 | +- Support for RR6's `PathMatch` objects and pattern matching |
| 38 | +- Handling of index routes and wildcard routes (`*`) |
| 39 | +- View identity tracking for parameterized routes (`/user/:id`) |
| 40 | +- Proper computation of parent paths for nested outlets |
85 | 41 |
|
86 | | -### 1. `routing.cy.js` – four failing tests |
| 42 | +**StackManager** was updated to work with the new view stack implementation. Changes include: |
| 43 | +- Parent path derivation using RR6's route matching |
| 44 | +- Improved handling of `Navigate` redirect components |
| 45 | +- Better coordination of entering/leaving views during transitions |
87 | 46 |
|
88 | | -The main routing demo is mostly green, but four tests still fail: |
| 47 | +### New Utilities |
89 | 48 |
|
90 | | -1. **Not-found route** – `/routing/asdf` is expected to render a `not-found` page and never does. This suggests that either the catch‑all route is not wired correctly for the RR6 matching rules, or the page is mounted but never becomes the active view item. |
91 | | -2. **Menu + redirect interaction** – the “Menu → Favorites → Menu → Home with redirect” flow expects Favorites to be hidden and Home to be visible; Cypress still finds the old page. This points to a corner case in how we hide/unmount views when a redirect happens off a menu selection. |
92 | | -3. **Back button visibility on pushed page** – after pushing a new page, the test expects the back button to appear. The view content appears, but we never hit the “show back button” state. This is likely a mismatch between our `LocationHistory` bookkeeping and how we decide whether the current view has something to go back to. |
93 | | -4. **Parameterized route instances** – the “mount new view item instances of parameterized routes” test times out with a value mismatch (`'1'` vs something else). This is the same area where we do view cloning for `/user/1` → `/user/2`; we may be reusing the wrong view item or not updating route params the way the test expects. |
| 49 | +Four utility modules were added to support RR6's routing model: |
94 | 50 |
|
95 | | -All of these are focused on how we track history and view identity, not on basic RR6 API usage. |
| 51 | +| File | Purpose | |
| 52 | +|------|---------| |
| 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 | |
96 | 57 |
|
97 | | -### 2. `swipe-to-go-back.cy.js` – one failing test |
| 58 | +### Test App Updates |
98 | 59 |
|
99 | | -Seven of the eight swipe tests pass. The remaining failure is: |
| 60 | +All test pages in `packages/react-router/test/base/src/pages/` were updated to use RR6 syntax: |
| 61 | +- `<Route path="/foo" component={Foo} />` became `<Route path="/foo" element={<Foo />} />` |
| 62 | +- Nested routes now require trailing wildcards (`path="parent/*"`) when they contain child outlets |
| 63 | +- `<Redirect to="..." />` became `<Navigate to="..." replace />` |
| 64 | +- Route params accessed via `useParams()` instead of `props.match.params` |
100 | 65 |
|
101 | | -- **“should swipe and go back to correct tab after switching tabs”** – after switching tabs and using the swipe gesture, we expect to land back on the correct tab page (`data-pageid=home`) but never see it. |
| 66 | +## Test Coverage |
102 | 67 |
|
103 | | -The other swipe tests (including aborting gestures and swiping within a tab) behave correctly, so the problem is very specific: combining tab switching with a swipe‑back is leaving the `LocationHistory` and/or active view in a slightly inconsistent state. |
| 68 | +The Cypress test suite covers the following scenarios: |
104 | 69 |
|
105 | | -### 3. `tab-context.cy.js` – both tests failing |
| 70 | +| Suite | Tests | Description | |
| 71 | +|-------|-------|-------------| |
| 72 | +| routing.cy.js | 29 | Core navigation, tabs, back button, redirects, params | |
| 73 | +| nested-outlets.cy.js | 11 | Nested `IonRouterOutlet` behavior, back navigation | |
| 74 | +| swipe-to-go-back.cy.js | 8 | Gesture navigation, abort handling, tab interactions | |
| 75 | +| 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 | |
| 78 | +| overlays.cy.js | 3 | Modal cleanup on navigation | |
| 79 | +| refs.cy.js | 2 | Ref forwarding to Ionic components | |
| 80 | +| tabs.cy.js | 2 | Basic tab navigation and history | |
| 81 | +| tab-context.cy.js | 2 | Programmatic tab switching via context | |
| 82 | +| dynamic-ionpage-classnames.cy.js | 1 | Dynamic class application to IonPage | |
| 83 | +| outlet-ref.cy.js | 1 | Ref access to IonRouterOutlet | |
| 84 | +| replace-actions.cy.js | 1 | History replacement behavior | |
106 | 85 |
|
107 | | -The small tab-context demo, which exercises `IonTabsContext.selectTab`, still fails both of its tests: |
| 86 | +## Known Limitations |
108 | 87 |
|
109 | | -1. Navigating from `/tab-context` and calling `selectTab('tab2')` tries to click a button that lives on a page still marked as hidden (`ion-page-hidden`). In other words, we update the active tab in context, but the underlying view stack has not actually switched visible pages. |
110 | | -2. Starting on `/tab-context/tab1` and trying to flip back to tab1 via context never finds the expected `tab2` page, which again points to view items not lining up with what the context thinks is active. |
| 88 | +### Route Path Syntax |
111 | 89 |
|
112 | | -The “normal” tabs test suite does not have this problem, so this looks isolated to how we bridge the context helper into the RR6 router + view stack. |
| 90 | +Nested outlets require parent routes to include a trailing wildcard: |
113 | 91 |
|
114 | | ---- |
| 92 | +```tsx |
| 93 | +// Correct |
| 94 | +<Route path="parent/*" element={<Parent />} /> |
115 | 95 |
|
116 | | -## Non-test gaps and cleanup work |
| 96 | +// Incorrect - child routes won't match |
| 97 | +<Route path="parent" element={<Parent />} /> |
| 98 | +``` |
117 | 99 |
|
118 | | -A few things that don’t show up as Cypress failures but are worth fixing before we put a label on this. |
| 100 | +This aligns with React Router 6's nested routing semantics where child routes are matched relative to the parent's path. |
119 | 101 |
|
120 | | -### TypeScript and types |
| 102 | +## Next Steps |
121 | 103 |
|
122 | | -`@ionic/react-router` is built against React Router 6, but the devDependencies still pull in v5 type packages: |
| 104 | +### Before Alpha Release |
123 | 105 |
|
124 | | -```json |
125 | | -"@types/react-router": "^5.0.3", |
126 | | -"@types/react-router-dom": "^5.1.5" |
127 | | -``` |
128 | | - |
129 | | -That hasn’t bitten us in the test app, but it’s confusing and will matter for downstream consumers. We should: |
130 | | - |
131 | | -- Drop the v5 type packages or replace them with the RR6 equivalents. |
132 | | -- Run a full type check across the package once that change is made. |
| 106 | +1. **Run a TypeScript strict check** on the `@ionic/react-router` package to catch any type regressions |
| 107 | +2. **Manual testing pass** through the test app to verify animations and gestures feel correct |
133 | 108 |
|
134 | 109 | ### Documentation |
135 | 110 |
|
136 | | -Right now the only “docs” for this work are the design document and this status file. Before releasing anything, we should have: |
137 | | - |
138 | | -- A short migration guide for going from the v5 integration to the v6 one. |
139 | | -- A clear explanation of any Ionic‑specific behavior that differs from plain React Router 6 (for example, `routeOptions.unmount`, how `LocationHistory` works, and how nested outlets should be declared with `/*`). |
| 111 | +The following documentation should be prepared before public release: |
140 | 112 |
|
141 | | -### Debug logging and general polish |
| 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 |
142 | 118 |
|
143 | | -During bring‑up we added logging in a few places to trace routing behavior. Before shipping we should: |
| 119 | +2. **Updated API reference** for: |
| 120 | + - `IonReactRouter`, `IonReactHashRouter`, `IonReactMemoryRouter` |
| 121 | + - `IonRouterOutlet` behavior with nested routes |
| 122 | + - `routeOptions.unmount` and `LocationHistory` behavior |
144 | 123 |
|
145 | | -- Audit `IonRouter`, `ReactRouterViewStack`, and `StackManager` for stray `console.log` / `console.warn` calls. |
146 | | -- Make sure any remaining logs are behind a flag or obviously diagnostic. |
| 124 | +### CI Integration |
147 | 125 |
|
148 | | ---- |
| 126 | +The GitHub Actions workflows have been updated to run the RR6 test app: |
| 127 | +- `.github/workflows/build.yml` |
| 128 | +- `.github/workflows/stencil-nightly.yml` |
149 | 129 |
|
150 | | -## Short-term plan |
| 130 | +## Architecture Reference |
151 | 131 |
|
152 | | -This is a realistic sequence of work to get from the current state to “alpha ready”: |
| 132 | +The data flow through the routing system: |
153 | 133 |
|
154 | | -1. **Close out the remaining E2E failures** |
155 | | - - Fix the not‑found and redirect/menu cases in `routing.cy.js`. |
156 | | - - Correct the back‑button logic and parameterized route instance handling in that same spec. |
157 | | - - Align swipe‑back behavior after tab switches with the existing (passing) swipe tests. |
158 | | - - Make `IonTabsContext.selectTab` and the RR6 router agree on which view is active in the tab-context demo. |
| 134 | +``` |
| 135 | +Browser History Change |
| 136 | + │ |
| 137 | + ▼ |
| 138 | + IonRouter (useLocation/useNavigate) |
| 139 | + │ |
| 140 | + ├── Updates LocationHistory |
| 141 | + ├── Computes RouteInfo (action, direction, params) |
| 142 | + │ |
| 143 | + ▼ |
| 144 | + RouteManagerContext |
| 145 | + │ |
| 146 | + ▼ |
| 147 | + StackManager (per IonRouterOutlet) |
| 148 | + │ |
| 149 | + ├── Derives parent path from route children |
| 150 | + ├── Matches routes using ReactRouterViewStack |
| 151 | + ├── Determines entering/leaving views |
| 152 | + │ |
| 153 | + ▼ |
| 154 | + ion-router-outlet.commit() |
| 155 | + │ |
| 156 | + ▼ |
| 157 | + Native Ionic Transition |
| 158 | +``` |
159 | 159 |
|
160 | | -2. **Clean up types and build configuration** |
161 | | - - Remove or update the v5 `@types/react-router*` packages. |
162 | | - - Run TypeScript with strict settings for `@ionic/react-router` and fix any fallout. |
| 160 | +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. |
163 | 161 |
|
164 | | -3. **Write the migration notes** |
165 | | - - Document the route shape expectations (e.g., `parent/*` when using nested outlets). |
166 | | - - Call out differences vs the v5 integration, especially around history behavior and any removed APIs. |
| 162 | +## Related Files |
167 | 163 |
|
168 | | -4. **Do a quick manual pass on the test app** |
169 | | - - Click through the main demos (tabs, nested outlets, overlays, swipe‑back) with an eye for anything the tests don’t currently cover. |
| 164 | +Source code: |
| 165 | +- `packages/react-router/src/ReactRouter/IonRouter.tsx` |
| 166 | +- `packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx` |
| 167 | +- `packages/react-router/src/ReactRouter/StackManager.tsx` |
| 168 | +- `packages/react-router/src/ReactRouter/utils/` |
170 | 169 |
|
171 | | -If we keep the changes tightly focused on these areas, we shouldn’t need to revisit the overall architecture again. |
| 170 | +Test app: |
| 171 | +- `packages/react-router/test/base/` (shared test code) |
| 172 | +- `packages/react-router/test/apps/reactrouter6/` (RR6 config) |
0 commit comments