Skip to content

Reactive Components ​

<ui-view> re-renders routed components automatically. But most apps also have components outside the viewport that depend on router state — a nav header highlighting the active section, a breadcrumb trail, a user menu that appears after login. Lit doesn't know these components care about the router, so by default they render once and go stale.

TransitionController is the core package's answer: a zero-dependency Lit ReactiveController that calls host.requestUpdate() whenever a matching transition event fires.

Basic usage ​

ts
import { html, LitElement } from 'lit';
import { TransitionController } from 'lit-ui-router';

class NavHeader extends LitElement {
  private transitions = new TransitionController(this);

  render() {
    // Re-evaluated after every successful transition
    return html`
      <span>Current state: ${this.transitions.current?.name}</span>
      ${this.transitions.includes('admin.**') ? html`<admin-toolbar></admin-toolbar>` : null}
    `;
  }
}

No wiring is needed: on hostConnected the controller discovers the router from the nearest <ui-router> (or <ui-view>) ancestor via the ui-router-context event, registers its transition hooks, and deregisters them all on hostDisconnected — nothing leaks when elements come and go from the DOM.

Reading router state ​

The controller exposes the essentials directly:

MemberWhat it returns
currentThe current StateDeclaration (globals.current)
paramsThe current parameter values (globals.params)
transitionThe most recent observed Transition
includes(state, p?)StateService.includes — supports glob patterns like 'admin.**'
router / globalsThe discovered UIRouter instance and its globals

Scoping with criteria and callbacks ​

By default the controller observes every successful transition. Options narrow that down and let you run logic before the re-render:

ts
class UserDetail extends LitElement {
  private transitions = new TransitionController(this, {
    criteria: { to: 'users.detail' },
    callback: () => this.loadUser(this.transitions.params.userId),
  });
}
  • criteria — a HookMatchCriteria limiting which transitions notify the host (to, from, glob patterns, predicate functions)
  • callback — invoked before requestUpdate() with the transition and the reason ('onSuccess', 'hostConnected', …)
  • events — which lifecycle events to observe; defaults to ['onSuccess'], and accepts any of 'onBefore' | 'onStart' | 'onSuccess' | 'onError'
  • router — an explicit router instance, skipping context discovery

For 'onBefore' and 'onStart' events, the callback's return value is passed back to UI-Router as a HookResult — so a controller can even cancel or redirect pending transitions.

Reconnect safety ​

On every (re)connect the controller synchronizes once with the router's current state before any new transition fires. Components that enter the DOM after navigation completed — or that are detached and re-attached, as with sticky states — render fresh values immediately instead of waiting for the next transition.

See it in a real app ​

The vanilla sample app is built on this pattern — its App, nav header, and message compose view each use a TransitionController. The behaviorally identical MobX sample app solves the same problems with the observable store and reaction controllers from lit-ui-router-mobx — if your app already uses MobX, prefer those bindings; the two codebases compare the idioms file-by-file.