lit-ui-router-effect
v0.1.3
Published
Effect bindings for lit-ui-router: a SubscriptionRef of the route and ref-following Lit ReactiveControllers
Readme
lit-ui-router-effect
Effect bindings for lit-ui-router: a SubscriptionRef of the route and ref-following 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 a SubscriptionRef and gives components a declarative, lifecycle-safe way to follow it (and any other SubscriptionRef) with automatic requestUpdate() — no manual refresh plumbing.
Features
routeRef(router)— aSubscriptionRef<RouteSnapshot>of the current state, params, and transition; one ref (and one transition hook) per router, attached lazily on first useRouterRefController— follows the route ref of the nearest<ui-router>context, discovered automatically through theui-router-contextevent; no prop drilling and no wiring in router configurationRefController— the generic primitive: selects over one or moreSubscriptionRefs while the host is connected; works with any ref, not just the router's- Lifecycle-safe — one fiber per controller, forked in
hostConnectedand interrupted inhostDisconnected; the value is re-read on every (re)connect so components that re-enter the DOM (e.g. sticky states) never render stale values - Renders before connect — refs given directly are read at construction, so a host rendered on the server sees the same value a browser would
Installation
npm install lit-ui-router-effect effect
# or
pnpm add lit-ui-router-effect effect
# or
yarn add lit-ui-router-effect effectlit-ui-router, lit, effect, and @uirouter/core are peer dependencies.
Quick Start
import { html, LitElement } from 'lit';
import { Data, Equal } from 'effect';
import { RouterRefController } from 'lit-ui-router-effect';
class AppNav extends LitElement {
// Re-renders only when a section's visibility actually flips —
// not on every transition. Data.struct gives the selection value
// equality, so Equal.equals compares it structurally.
private active = new RouterRefController(
this,
(route) =>
Data.struct({
inbox: route.includes('inbox.**'),
contacts: route.includes('contacts.**'),
}),
{ equals: Equal.equals },
);
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 routeRef(router) lazily attaches the ref's single transition hook on first use.
API
routeRef(router)
The SubscriptionRef<RouteSnapshot> for a router — memoized, one per router instance. The first call registers one transitionService.onSuccess hook that replaces the value per successful transition.
RouteSnapshot
A value taken once per successful transition. Every member answers for that moment, so a selector asking during a later transition gets the settled answer, not the in-flight one.
| Member | Description |
| --------------------------- | ---------------------------------------------------------------------------- |
| current | The current StateDeclaration (globals.current) |
| params | The current RawParams (globals.params), a fresh object per transition |
| transition | The transition that produced the snapshot |
| includes(stateOrName, p?) | StateService.includes evaluated against the snapshot (globs like 'a.**') |
RouterRefController
A ReactiveController that follows the route ref of the host's <ui-router> context:
new RouterRefController(host, selector, options?)selector: (route: RouteSnapshot) => T— the selected expression; the result is exposed as.valueoptions.router— explicit router instance, skipping context discovery; the route is then read at construction, so.valueis live before the host connectsoptions.onChange— effect invoked when the selected value changes (and once on every (re)connect); useful for resetting component state from route paramsoptions.equals— comparer for precise, value-based change detection (Equal.equalsforDatavalues, or any(a, b) => boolean); defaults toObject.isoptions.initialValue— the value.valuecarries before the router is discovered: beforehostConnected, and while a host has no router contextoptions.runtime— the runtime the subscription fiber is forked on; defaults to Effect's default runtime, and aManagedRuntimesatisfies it directly
RefController
The generic primitive behind RouterRefController — the same selector/options contract over any SubscriptionRefs:
import { Data, Equal } from 'effect';
import { RefController } from 'lit-ui-router-effect';
class NavHeader extends LitElement {
private auth = new RefController(
this,
[Session.user$, Session.loggedIn$],
(user, loggedIn) => Data.struct({ user, loggedIn }),
{ equals: Equal.equals },
);
render() {
const { user, loggedIn } = this.auth.value;
// ...
}
}The refs are a tuple, and the selector receives their values positionally. Pass a thunk instead (() => [ref]) for refs that depend on the host's place in the DOM; it is resolved on every hostConnected, and .value carries options.initialValue until then.
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 RouterRefController whose host has no <ui-router> ancestor warns once, naming that host, and follows 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.
