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 live

The same problem, solved with the MobX bindings: <app-root> sits outside every <ui-view>, so it never receives fresh view props, and reaction controllers keep its breadcrumb and visit counter in sync. Swap RouterReactionController for TransitionController and the shape is identical — a controller on the host, a value read in render().

In both idioms the controllers only read. The example's one write to app state — recording an arrival — sits in an onSuccess hook, because an effect caused by navigating belongs to the navigation, not to a component watching it.

This is the minimal version — the solar-system tutorial with a store layered in where the router stops helping, small enough to read in one sitting. For the full comparison, the two sample apps below build the same application twice, once with each idiom.

Open in StackBlitz

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, one of the companion packages — if your app already uses MobX, prefer those bindings; the two codebases compare the idioms file-by-file.