DEV Community

Chris Maas
Chris Maas

Posted on

Stencil: Routing with ion-router, ion-tabs, and how to pass params to tab pages (without using Angular)

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
    ];
  }
}
Enter fullscreen mode Exit fullscreen mode

This setup does the following:

  • When your app starts under localhost:3333, it will redirect to localhost:3333/home via a <ion-route-redirect>
  • name is passed as a URL parameter to app-profile and received as a Prop()
  • 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>
Enter fullscreen mode Exit fullscreen mode

This will result in the following error:

[ion-router] URL is not part of the routing set
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }} />
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
cm profile image
Chris Maas

I figured, there's still a somewhat unexpected behavior, which I haven't figured out yet. The params of app-profile actually cause app-profile to be destroyed and recreated. I'm unsure if this behavior is intended. I want to re-use the tab app-profile. Will post again, if I found a solution that doesn't cause app-profile to be re-created.

Collapse
 
francoisbelanger profile image
Francois Belanger

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

Collapse
 
seanmclem profile image
Seanmclem

How different is this than the ionic-stencil boilerplate made by the CLI?

Collapse
 
cm profile image
Chris Maas

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.

Collapse
 
jericam profile image
Erica Marchi

Hi, can you please make a repo of this? I have some doubts about various points ^

Collapse
 
cm profile image
Chris Maas

I updated the article with a bunch of observations on how params re-create the target page.

Collapse
 
bitflowertweets profile image
bitflower

Repo? Would be aaaawesome ;-)

Collapse
 
cm profile image
Chris Maas

Sure, will post tomorrow, gotta go for today. But honestly, you only need to modify two files ;)