Setting up <ion-router> in combination with <ion-tabs> in a Stencil-only project (without Angular) can be quite tricky. This article covers:
- How to use <ion-router>with<ion-tabs>
- How to assign a rootroute with<ion-tabs>, especially if no tab has the URL/
- How to pass parameters to tab pages without using Angular
Prerequisite: Install ionic-pwa
Start with the ionic-pwa starter template:
npm init stencil
→ select ionic-pwa
  
  
  How to use ion-router with ion-tabs
First, here’s the full working example with all features. It works when switching between tabs, when calling a route through a button and when reloading the browser with a different URL. Later, I show you what doesn’t quite work.
Modify app-root.tsx:
<ion-app>
  <ion-router useHash={false}>
    <ion-route-redirect from="/" to="/home" />
    <ion-route component="app-tabs">
      <ion-route url="/home" component="tab-home">
        <ion-route component="app-home" />
      </ion-route>
      <ion-route url="/profile" component="tab-profile">
        <ion-route url="/:name" component="app-profile" />
      </ion-route>
    </ion-route>
  </ion-router>
  <ion-nav />
</ion-app>
Add a new file app-tabs.tsx:
import { Component, h } from '@stencil/core';
@Component({
  tag: 'app-tabs'
})
export class AppTabs {
  render() {
    return [
      <ion-tabs>
        <ion-tab tab="tab-home">
          <ion-nav />
        </ion-tab>
        <ion-tab tab="tab-profile">
          <ion-nav />
        </ion-tab>
        <ion-tab-bar slot="bottom">
          <ion-tab-button tab="tab-home">
            <ion-icon name="home" />
            <ion-label>home</ion-label>
          </ion-tab-button>
          <ion-tab-button tab="tab-profile" href="/profile/notangular">
            <ion-icon name="person" />
            <ion-label>Profile</ion-label>
          </ion-tab-button>
        </ion-tab-bar>
      </ion-tabs>
    ];
  }
}
This setup does the following:
- When your app starts under localhost:3333, it will redirect tolocalhost:3333/homevia a<ion-route-redirect>
- 
nameis passed as a URL parameter toapp-profileand received as aProp()
- Tapping the tab also passes a parameter to app-profile
What doesn’t work
  
  
  root-attribute in ion-router instead of ion-route-redirect
<ion-app>
  <ion-router useHash={false} root="/home">
    <ion-route component="app-tabs">
      <ion-route url="/home" component="tab-home">
        <ion-route component="app-home" />
      </ion-route>
      <ion-route url="/profile" component="tab-profile">
        <ion-route url="/:name" component="app-profile" />
      </ion-route>
    </ion-route>
  </ion-router>
  <ion-nav />
</ion-app>
This will result in the following error:
[ion-router] URL is not part of the routing set
Why? Probably because <ion-router> has only one direct child route, which doesn’t have a url attribute. When the router is set up, the root is unknown, because it is nested somewhere in the tabs route (note: this is my assumption, I didn’t check the code).
Catch-all child route never receives the query parameter
<ion-app>
  <ion-router useHash={false}>
    <ion-route-redirect from="/" to="/home" />
    <ion-route component="app-tabs">
      <ion-route url="/home" component="tab-home">
        <ion-route component="app-home" />
      </ion-route>
      <ion-route url="/profile/:name" component="tab-profile">
        <ion-route component="app-profile" />
      </ion-route>
    </ion-route>
  </ion-router>
  <ion-nav />
</ion-app>
In this case, name is passed to the tab-profile component, but not to app-profile.
Two child routes to make the parameter optional
I experimented with this setup to make the query parameter optional when using <ion-router>:
<ion-app>
  <ion-router useHash={false}>
    <ion-route-redirect from="/" to="/home" />
    <ion-route component="app-tabs">
      <ion-route url="/home" component="tab-home">
        <ion-route component="app-home" />
      </ion-route>
      <ion-route url="/profile" component="tab-profile">
        <ion-route url="/" component="app-profile" />
        <ion-route url="/:name" component="app-profile" />
      </ion-route>
    </ion-route>
  </ion-router>
  <ion-nav />
</ion-app>
At first, this seems to work. You can call /profile and /profile/ionic and both render just fine. However, when I attached a bunch of console.log statements to constructor(), componentDidLoad() and componentDidUnload(), it showed that the app-profile page gets created twice. It’s also unloaded at some point, although there is no noticeable visual difference. The behavior could generally be described as flaky.
I didn’t figure out how to make the query parameter truly optional, but you could simply pass some generic value by setting the href attribute of ion-tab-button:
<ion-tab-button tab="tab-profile" href="/profile/not-a-person">
Then, in app-profile, parse the Prop name and if it is equal to not-a-person do something else.
  
  
  Putting the url in the child route
If you move the url attribute to the child route like so:
<ion-app>
  <ion-router useHash={false}>
    <ion-route component="app-tabs">
      <ion-route url="/" component="tab-home">
        <ion-route component="app-home" />
      </ion-route>
      <ion-route component="tab-profile">
        <ion-route url="/profile/:name" component="app-profile" />
      </ion-route>
    </ion-route>
  </ion-router>
  <ion-nav />
</ion-app>
You can expect flaky behavior again. This setup works in some cases. If you click on the profile tab, it won’t show, but if you open the app-profile page through pressing the button on app-home, it seems to work and passes the prop. Then, if you hit the tab again, it shows the page, although the browser URL isn’t changed and from a component lifecycle view, the page actually should have unloaded. So, this doesn’t really work.
  
  
  Good to know: Observations about ion-router
Different parameter = different component
Consider this route:
<ion-route url="/profile/:name" component="app-profile" />
If you open /profile/user1 through a button and then /profile/user2 through another button (assuming you use tabs or a side menu to navigate away from the screen you just opened), Stencil will create a new app-profile component (and destroy the old one). It does not update the Prop() name of app-profile.
Hashes with route params won’t work
The same is true in this case:
/profile/user1#a
/profile/user1#b
The parameter name has the value user1#a or user1#b. It’s not the same, as you might expect from traditional server-side routing in web apps.
Tabs with same name (and no parameter) get reused
If you have a tab app-profile with the route /profile, it doesn’t matter if you open /profile from a button or a tab. The very same component will be shown and not re-created. Thus, it maintains its state, even when you move away from that page through a different tab.
  
  
  Routes with componentProps create new components
Consider this route, which passes props via componentProps:
<ion-route url="/profile" component="app-profile" componentProps={{ name: this.name }} />
If this.name changes, the app-profile component will be destroyed and a new component created with the new name. It does not update the props of the existing app-profile.
  
  
  Is there a way to have a global app-profile component that gets reused when parameters or props change?
Why would anyone need this? You need this when rendering a page is expensive. Let’s say you have a mapping library. Rendering the map of a city takes a few seconds. You want to keep the page-citymap the same for performance reasons, but you also want to reflect the currently selected city in the URL like so: /map/berlin or /map/nyc. As far as I can see, this doesn’t work with the current ion-router. If you navigate to a different URL, page-citymap would be recreated and the expensive map-rendering would start again for the new component.
Correct me if I’m wrong and if you have found a solution for this.
One more note: Have a look at the Ionic Stencil Conference App, which shows how to use route parameters in a master-detail scenario.
 

 
    
Top comments (8)
I figured, there's still a somewhat unexpected behavior, which I haven't figured out yet. The params of
app-profileactually causeapp-profileto be destroyed and recreated. I'm unsure if this behavior is intended. I want to re-use the tabapp-profile. Will post again, if I found a solution that doesn't causeapp-profileto be re-created.Great article,
just a quick note that I'm re-using the same page by simply using a variable within a global service. I store the required values within that service and read them back from within the page. A bit dirty...
I updated the article with a bunch of observations on how params re-create the target page.
How different is this than the ionic-stencil boilerplate made by the CLI?
The boilerplate doesn’t use tabs. Using tabs + router + params together is a bit tricky.
The difference in code is just a few lines of code.
Hi, can you please make a repo of this? I have some doubts about various points ^
Repo? Would be aaaawesome ;-)
Sure, will post tomorrow, gotta go for today. But honestly, you only need to modify two files ;)