Stencil: Routing with ion-router, ion-tabs, and how to pass params to tab pages (without using Angular)
Tags: javascript, typescript, ionic, stencil
Published: 2019-09-11 12:42:35

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

```html
<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`:

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

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

```html
<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>`:

```html
<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`: 

```html
<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: 

```html
<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:

```html
<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`:

```html
<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](https://github.com/ionic-team/ionic-stencil-conference-app), which shows how to use route parameters in a master-detail scenario.