@praxisjs/router
Changelog for @praxisjs/router — client-side routing with @Router, @Route, @Lazy, @Params, and @Location.
@praxisjs/router
2.3.2
Depends on the other @praxisjs/* packages through caret ranges (^2.3.0) instead of exact versions, so npm no longer installs a second copy of a package when two releases disagree, and upgrades no longer end in peer conflicts.
2.3.0
The published package no longer contains the package's tests (compiled tests in dist/__tests__, test sources in src/__tests__).
The package is marked "sideEffects": false, so bundlers leave out the modules of it that an app does not use.
Bug fix: a Link with no class prop removes its class attribute when it stops being active, instead of leaving class="" behind.
@Params, @Query, @Location and @InjectLayout throw a TypeError when the class is defined if they decorate an accessor instead of a plain field, naming the member to fix (see assertFieldContext).
New router.isActive(path): whether path is the current location's path. Inside an effect it re-runs only when that path becomes or stops being the current one. Link uses it, so a navigation re-evaluates the active class of two links instead of every link on the page. New router.destroy() removes the router's popstate listener; createRouter() calls it on the router it replaces.
RouterView keeps the layout mounted when a navigation keeps the same layout, and swaps only the page inside it. Before, every navigation rebuilt the layout too, losing its state (an open sidebar, a scroll position, an input).
Static route paths are now looked up by key and dynamic ones matched with a single regex run, with declaration order still deciding which route wins. @Route sets its metadata on the class without wrapping it in a subclass, and @Params, @Query, @Location, @InjectLayout, @Meta and @Router() share one accessor across instances instead of building one per instance.
Bug fix: characters such as ., +, ( or ) in a route path are now matched literally. /v1.0/docs used to also match /v1x0/docs, and a path with parentheses never matched itself.
Bug fix: a router replaced by a new createRouter() call (SSG prerender, or a re-mounted @Router app) kept listening to popstate and navigating in the background.
2.2.1
The router now resolves a route's component and layout concurrently and applies them in a single update. Before, a route with lazy page and layout chunks loaded them one after the other, and RouterView rebuilt the page twice — first without its layout, then again once the layout arrived.
Plain components, and lazy ones already loaded, are applied synchronously: the initial route is in place when createRouter returns, and revisiting a loaded lazy route has no microtask gap. A layout that fails to load still rejects the navigation, but the page is shown first. Route matching also runs once per path instead of once per lookup.
2.2.0
Exposed createRouter from @praxisjs/router/internal. @praxisjs/ssg's prerender runner needs to rebind the router to a fresh window.location before rendering each route — the @Router([...]) class decorator only calls createRouter once, at the first time the app module is imported.
Bug fix: the router's initial navigation is now tracked against @praxisjs/core's server-render resource barrier. Previously, prerendering a Lazy(...)-wrapped route with @praxisjs/ssg could serialize the page — and close the underlying Vite SSR module runner — before that route's dynamic import had resolved, producing pages with missing content and an unhandled rejection that crashed the build once the import finally settled. Client-side navigation is unaffected.
2.1.0
Named routes — give routes a name and navigate by name instead of hardcoding paths.
name?: stringfield onRouteDefinitionand on the@Route({ path, name, meta })options objectpush({ name, params?, query?, hash? })andreplace({ name, ... })— navigate by name; params are interpolated into the path patternrouter.resolvePath({ name, params? })— convert a named target to a path string without navigatingNamedNavigationTargetandNavigationInputtypes exported from the package<Link to={{ name, params?, query?, hash? }}>—toprop now acceptsstring | NamedNavigationTarget
Route meta — attach arbitrary metadata to any route definition.
meta?: Record<string, unknown>field onRouteDefinition/@Routeoptionslocation().meta— current route meta object (defaults to{}when unset)location().name— current route name (string | undefined)router.meta— reactiveComputed<RouteMeta>shorthand forlocation().meta@Meta()— new field decorator; injectsrouter.metainto any component fielduseMeta()— composable returningrouter.metaRouteMetatype exported from the package
Navigation lifecycle hooks — post-navigation callbacks for analytics, focus management, and side effects.
afterEnter?: (to, from) => void— per-route hook inRouteDefinition; fires after the component is resolved, before globalafterEachhandlersrouter.afterEach(handler)— global hook; fires after every completed navigation (not called whenbeforeEnterblocks); returns an unregister function- Hook execution order:
beforeEnter → component resolved → afterEnter → afterEach → scrollBehavior
Scroll restoration — configurable scroll behavior per navigation.
scrollBehavior?: ScrollBehavioroption added toRouterOptions/createRouter/@Router- Receives
(to, from, savedPosition)—savedPositionis non-null on back/forward navigations - Return
{ top, left }to scroll to coordinates,{ el }to scroll an element into view, orfalseto skip scrolling; can return aPromise - Scroll position of the departing page is automatically saved into
history.statebefore eachpush(); restored viasavedPositionon popstate - Exported:
ScrollPosition,ScrollBehavior,SavedScrollPosition,RouterOptions
2.0.0
Breaking — route type aliases changed
RouteParams, RouteQuery, and RouteLocation are now signal/computed types. Field annotations no longer require importing Signal or Computed.
| Decorator | Before | After |
|---|---|---|
@Params() | Computed<RouteParams> | RouteParams |
@Query() | Computed<RouteQuery> | RouteQuery |
@Location() | Signal<RouteLocation> | RouteLocation |
@InjectLayout() | Signal<RouteComponent | null> | LayoutInstance |
If you typed beforeEnter guards explicitly with the old RouteLocation, remove the annotation (TypeScript infers from context) or use Parameters<RouteDefinition['beforeEnter']>[0].
1.2.0
Optional route segments — append ? to any named segment to make it optional.
{ path: '/posts/:slug?', component: PostPage }/posts/hello-world→params().slug === 'hello-world'/posts/→params().slug === ''(absent segment defaults to empty string)
1.1.0
Breaking — API renames
| Changed | Before | After |
|---|---|---|
| Root app decorator | @RouterConfig([...]) | @Router([...]) |
| Router field decorator | @InjectRouter() router!: Router | @Router() router!: RouterInstance |
| Router class (type annotation) | Router | RouterInstance |
| Class export | export { Router } | export { RouterInstance } |
Layout system — routes can now declare a persistent shell component that wraps the matched page without re-mounting on navigation.
layout?property onRouteDefinition— attach a layout to any route directly;RouterViewrenders<Layout>around the matched page- Layout inheritance via
children— when a route has child routes, itscomponentautomatically becomes the layout for all of them <RouterOutlet>— new component for use inside layouts (StatelessComponent); reactively renders the current page without needing thechildrenprop@InjectLayout()— field decorator injectingRouterInstance.currentLayoutas a reactive signal, following the same pattern as@Router(),@Params(), etc.RouterInstance.currentLayout— reactive signal exposing the currently active layout component- Lazy layouts —
layout: Lazy(() => import('./MyLayout'))works identically to lazy page components
@Router decorator (renamed from @RouterConfig) — now implemented with createClassDecorator and ClassBehavior, consistent with the rest of the decorator API. RouterBehavior.create() re-activates the correct router on each component instantiation, resolving singleton conflicts when multiple @Router-decorated classes share the same module scope (e.g. Storybook stories loaded together).
Navigation sequence — concurrent push() calls no longer race. A monotonic _navSeq counter ensures only the most recent navigation commits its result; stale in-flight resolutions are silently discarded.
1.0.13
Updated dependencies — @praxisjs/[email protected], @praxisjs/[email protected], @praxisjs/[email protected].
1.0.12
<Link> now accepts a style prop — both a CSS string (style="color:red") and a style object (style={styleObject}).
1.0.1
@Lazyinline fix —Lazy(() => import('./Page'))used directly in route definitions now renders correctly. The returned function was missing the__isLazymarker so the router treated it as a plain component@Routeco-location —@RouterConfignow accepts classes decorated with@Routedirectly. The path is read from__routePathautomatically — no{ path, component }wrapper needed- Lazy-loaded pages must use
export default class— the loader resolvesmodule.defaultat runtime
1.0.0
Breaking — full decorator-based API
All functional routing APIs removed:
| Removed | Replacement |
|---|---|
createRouter() | @RouterConfig on the root component |
lazy() | @Lazy decorator or Lazy() inline |
useRouter() | @Router() field decorator |
useParams() | @Params() field decorator |
useQuery() | @Query() field decorator |
useLocation() | @Location() field decorator |
0.2.4
Bug fix: beforeEnter guards that return a redirect string no longer recurse infinitely. Navigation aborts after 10 redirect hops with a console warning.
0.2.0
Migrated to TC39 decorator context API. Introduced StatefulComponent / StatelessComponent.
0.1.0
Initial beta release.