loading...

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

cm profile image Chris Maas ・5 min read

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

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.

Posted on by:

cm profile

Chris Maas

@cm

Guy with laptop interested in Ionic, Stencil, TypeScript, CouchDB, and offline-first app development.

Discussion

markdown guide
 

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.

 

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.

 

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

 

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

 
 

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