Routing
Ionic uses framework-native routers (Angular Router, React Router, Vue Router) under an IonRouterOutlet for native-feeling stack transitions and back-gesture support.
Angular + React + Vue patterns
EXAMPLE
<!-- === Angular === -->
<!-- app-routing.module.ts -->
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import { authGuard, adminGuard } from './guards';
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
// Lazy-load by component
{ path: 'home', loadComponent: () => import('./home/home.page').then(m => m.HomePage) },
{ path: 'login', loadComponent: () => import('./login/login.page').then(m => m.LoginPage) },
// Route params
{ path: 'post/:id', loadComponent: () => import('./post/post.page').then(m => m.PostPage) },
// Guards
{ path: 'account',
loadComponent: () => import('./account/account.page').then(m => m.AccountPage),
canActivate: [authGuard],
},
{ path: 'admin',
loadChildren: () => import('./admin/admin.routes').then(m => m.routes),
canActivate: [adminGuard],
},
// Tabs container
{ path: 'tabs',
loadComponent: () => import('./tabs/tabs.page').then(m => m.TabsPage),
children: [
{ path: '', redirectTo: 'feed', pathMatch: 'full' },
{ path: 'feed', loadComponent: () => import('./feed/feed.page').then(m => m.FeedPage) },
{ path: 'profile', loadComponent: () => import('./profile/profile.page').then(m => m.ProfilePage) },
],
},
// Catch-all
{ path: '**', loadComponent: () => import('./not-found/not-found.page').then(m => m.NotFoundPage) },
];
@@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules, // preload after navigation
scrollPositionRestoration: 'enabled',
})],
exports: [RouterModule],
})
export class AppRoutingModule {}
<!-- app.component.html -->
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>
<!-- Navigate programmatically (Angular) -->
import { NavController } from '@ionic/angular';
constructor(private nav: NavController) {}
openPost(id: string) {
this.nav.navigateForward(`/post/${id}`, { animated: true });
}
back() {
this.nav.navigateBack('/home');
}
replaceWithRoot(url: string) {
this.nav.navigateRoot(url); // resets the stack
}
<!-- Read params -->
import { ActivatedRoute } from '@angular/router';
constructor(private route: ActivatedRoute) {
this.id = this.route.snapshot.paramMap.get('id')!;
}
// Or subscribe to changes:
this.route.paramMap.subscribe(p => this.id = p.get('id'));
<!-- Query params -->
this.nav.navigateForward('/search', { queryParams: { q: 'docker' } });
this.route.snapshot.queryParamMap.get('q');
<!-- Lifecycle hooks (Ionic-specific) -->
ionViewWillEnter() {} // before page becomes active
ionViewDidEnter() {} // after page is shown
ionViewWillLeave() {} // before navigating away
ionViewDidLeave() {} // after navigated away
<!-- === React (Ionic React) === -->
import { IonReactRouter } from '@ionic/react-router';
import { IonApp, IonRouterOutlet, IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/react';
import { Route, Redirect } from 'react-router-dom';
import { home, person } from 'ionicons/icons';
export default function App() {
return (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/home" component={HomePage} />
<Route path="/post/:id" component={PostPage} />
<Route path="/tabs" component={TabsPage} />
<Route exact path="/" render={() => <Redirect to="/home" />} />
<Route component={NotFoundPage} />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
);
}
function TabsPage() {
return (
<IonTabs>
<IonRouterOutlet>
<Route exact path="/tabs/feed" component={FeedPage} />
<Route exact path="/tabs/profile" component={ProfilePage} />
<Route exact path="/tabs" render={() => <Redirect to="/tabs/feed" />} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="feed" href="/tabs/feed">
<IonIcon icon={home} /><IonLabel>Feed</IonLabel>
</IonTabButton>
<IonTabButton tab="profile" href="/tabs/profile">
<IonIcon icon={person} /><IonLabel>Profile</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
}
import { useHistory, useParams } from 'react-router-dom';
function PostPage() {
const history = useHistory();
const { id } = useParams<{ id: string }>();
return <IonButton onClick={() => history.push('/home')}>Back</IonButton>;
}
<!-- === Vue (Ionic Vue) === -->
import { createRouter, createWebHistory } from '@ionic/vue-router';
import HomePage from './pages/HomePage.vue';
const routes = [
{ path: '/', redirect: '/home' },
{ path: '/home', component: HomePage },
{ path: '/post/:id', component: () => import('./pages/PostPage.vue') },
{ path: '/tabs', component: () => import('./pages/TabsPage.vue'),
children: [
{ path: '', redirect: 'feed' },
{ path: 'feed', component: () => import('./pages/FeedPage.vue') },
{ path: 'profile', component: () => import('./pages/ProfilePage.vue') },
],
},
];
export const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
// main.ts
import { IonicVue } from '@ionic/vue';
import { createApp } from 'vue';
import App from './App.vue';
import { router } from './router';
const app = createApp(App).use(IonicVue).use(router);
router.isReady().then(() => app.mount('#app'));
// App.vue
<template>
<ion-app>
<ion-router-outlet />
</ion-app>
</template>
// Navigate (Composition API)
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute();
router.push(`/post/${id}`);
router.back();
router.replace('/home');
const id = computed(() => route.params.id);
<!-- === Common features === -->
<!-- Back button (auto-detects history) -->
<ion-back-button defaultHref="/home"></ion-back-button>
<!-- Modal that doesn't change URL -->
import { ModalController } from '@ionic/angular';
async presentModal() {
const m = await this.modalCtrl.create({ component: EditPage });
await m.present();
}
<!-- Deep linking (Capacitor + universal links) -->
<!-- iOS: AASA (apple-app-site-association) file -->
<!-- Android: assetlinks.json + intent-filter in manifest -->
<!-- Web: handled automatically by the router -->
<!-- Guards -->
import { CanActivateFn } from '@angular/router';
export const authGuard: CanActivateFn = (route, state) => {
if (auth.isLoggedIn()) return true;
return router.parseUrl(`/login?return=${state.url}`);
};
<!-- === Common bugs === -->
<!-- - Using <router-outlet> instead of <ion-router-outlet> → loses native gestures -->
<!-- - Tabs need IonTabs around BOTH IonTabBar and IonRouterOutlet -->
<!-- - Modal closes via dismiss(), not via router navigation -->
<!-- - Capacitor deep links require platform-specific config files -->
<!-- - On Android, hardware back button needs explicit handler in some cases -->
Why it matters
Use IonRouterOutlet instead of the framework’s default outlet — it’s what gives Ionic apps the native stack-swipe-back gesture and page transitions. Pair with guards + lazy loading for performance.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// React
import { IonReactRouter } from '@ionic/react-router';
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/home" component={Home} />
<Route exact path="/profile/:id" component={Profile} />
</IonRouterOutlet>
</IonReactRouter>
Try it Yourself »
Exercise
Route outlet element in Ionic React.
<
>{routes}</
>
PascalCase.
Discussion
Loading…