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
root
route 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/home
via a<ion-route-redirect>
-
name
is passed as a URL parameter toapp-profile
and 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-profile
actually causeapp-profile
to 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-profile
to 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...
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 ^
I updated the article with a bunch of observations on how params re-create the target page.
Repo? Would be aaaawesome ;-)
Sure, will post tomorrow, gotta go for today. But honestly, you only need to modify two files ;)