lit-ui-router-mobx
v1.0.2
Published
MobX bindings for lit-ui-router: an observable router store and reaction-based Lit ReactiveControllers
Readme
lit-ui-router-mobx
MobX bindings for lit-ui-router: an observable router store and reaction-based Lit ReactiveControllers.
A thin wrapper on top of lit-ui-router — it registers no custom elements and adds no routing behavior. It mirrors the router's state into MobX observables and gives components a declarative, lifecycle-safe way to observe them (and any other MobX state) with automatic requestUpdate() — no manual refresh plumbing.
Features
RouterStore— an observable mirror of the current state, params, and transition; one store (and one transition hook) per router viaRouterStore.for(router)RouterReactionController— observes the store of the nearest<ui-router>context, discovered automatically through theui-router-contextevent; no prop drilling and no store wiring in router configurationReactionController— the generic primitive: runs a MobXreactionover an explicit selector while the host is connected; works with any MobX observables, not just the router- Lifecycle-safe — reactions are created in
hostConnectedand disposed inhostDisconnected; they fire immediately on (re)connect so components that re-enter the DOM (e.g. sticky states) never render stale values
Installation
npm install lit-ui-router-mobx mobx
# or
pnpm add lit-ui-router-mobx mobx
# or
yarn add lit-ui-router-mobx mobxlit-ui-router, lit, mobx, and @uirouter/core are peer dependencies.
MobX 6 and 7
Both majors are supported (mobx@^6.0.0 || ^7.0.0) and both are exercised in
CI. The bindings' own API is identical on either. Note that MobX 7 replaced the
namespaced comparers with named exports, so the equals option in the examples
below is spelled compareStructural on 7 and comparer.structural on 6 —
equals accepts any (a, b) => boolean, so either works.
Quick Start
import { html, LitElement } from 'lit';
import { RouterReactionController } from 'lit-ui-router-mobx';
class AppNav extends LitElement {
// Re-renders only when the section's visibility actually flips —
// not on every transition.
private active = new RouterReactionController(this, (route) => ({
inbox: route.includes('inbox.**'),
contacts: route.includes('contacts.**'),
}));
render() {
return html`...${this.active.value.inbox ? 'Inbox is open' : ''}...`;
}
}No router configuration is required: the controller discovers the router from the enclosing <ui-router> element on hostConnected, and RouterStore.for(router) lazily attaches the store's single transition hook on first use.
API
RouterStore
An observable mirror of a router's current state, updated by one transitionService.onSuccess hook.
| Member | Description |
| --------------------------- | ---------------------------------------------------------------------------------------------------------- |
| RouterStore.for(router) | The store for a router — memoized, one per router instance |
| current | The current StateDeclaration (globals.current) |
| params | The current RawParams (globals.params), replaced per transition |
| transition | The most recent successful Transition |
| includes(stateOrName, p?) | Observable version of StateService.includes (supports globs like 'a.**') |
| attach(router) | Manual attachment, for self-managed store instances — idempotent per router, and returns a detach function |
RouterReactionController
A ReactiveController that observes the RouterStore of the host's <ui-router> context:
new RouterReactionController(host, selector, options?)selector: (store: RouterStore) => T— the observed expression; the result is exposed as.valueoptions.router— explicit router instance, skipping context discoveryoptions.onChange— effect invoked when the selected value changes (and once on every (re)connect); useful for resetting component state from route paramsoptions.equals— MobX comparer (e.g.compareStructural) for precise, value-based change detectionoptions.initialValue— the value.valuecarries before the first reaction run: beforehostConnected, and while a host has no router context
ReactionController
The generic primitive behind RouterReactionController — the same selector/options contract over any MobX observables:
import { compareStructural } from 'mobx';
import { ReactionController } from 'lit-ui-router-mobx';
class NavHeader extends LitElement {
private auth = new ReactionController(
this,
() => ({ user: SessionStore.user, loggedIn: SessionStore.loggedIn }),
{ equals: compareStructural },
);
render() {
const { user, loggedIn } = this.auth.value;
// ...
}
}Development and production builds
dist/development/index.js is published alongside dist/index.js and picked by the development export condition, which bundlers resolve automatically in development; production builds get the default. Nothing to configure. lit-ui-router ships the same split — see the Development & Production Builds guide for the mechanism and the warnings both packages carry.
The development build adds one console warning here: a RouterReactionController whose host has no <ui-router> ancestor warns once, naming that host, and observes nothing — .value stays at options.initialValue, so the host renders once and never again. Wrap the subtree in <ui-router>, or pass options.router for a host outside the router's DOM. Production builds drop the warning and its message text, and lit's own production build silences it as well.
Why selectors instead of render auto-tracking?
Mixins like MobxLitElement auto-track every observable read in render(). The controllers here are the composition-friendly alternative:
- No base class required — controllers attach to any
LitElement(or anyReactiveControllerHost) - Dependencies are explicit: the selector names exactly which observables drive the host
equals: compareStructuralavoids re-renders when a recomputed value is structurally unchanged- The reaction lifecycle is bound to the host's connection lifecycle automatically
