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.