Morphos & Kosmesis are here.Explore
PraxisJS

@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?: string field on RouteDefinition and on the @Route({ path, name, meta }) options object
  • push({ name, params?, query?, hash? }) and replace({ name, ... }) — navigate by name; params are interpolated into the path pattern
  • router.resolvePath({ name, params? }) — convert a named target to a path string without navigating
  • NamedNavigationTarget and NavigationInput types exported from the package
  • <Link to={{ name, params?, query?, hash? }}> — to prop now accepts string | NamedNavigationTarget

Route meta — attach arbitrary metadata to any route definition.

  • meta?: Record<string, unknown> field on RouteDefinition / @Route options
  • location().meta — current route meta object (defaults to {} when unset)
  • location().name — current route name (string | undefined)
  • router.meta — reactive Computed<RouteMeta> shorthand for location().meta
  • @Meta() — new field decorator; injects router.meta into any component field
  • useMeta() — composable returning router.meta
  • RouteMeta type exported from the package

Navigation lifecycle hooks — post-navigation callbacks for analytics, focus management, and side effects.

  • afterEnter?: (to, from) => void — per-route hook in RouteDefinition; fires after the component is resolved, before global afterEach handlers
  • router.afterEach(handler) — global hook; fires after every completed navigation (not called when beforeEnter blocks); returns an unregister function
  • Hook execution order: beforeEnter → component resolved → afterEnter → afterEach → scrollBehavior

Scroll restoration — configurable scroll behavior per navigation.

  • scrollBehavior?: ScrollBehavior option added to RouterOptions / createRouter / @Router
  • Receives (to, from, savedPosition) — savedPosition is non-null on back/forward navigations
  • Return { top, left } to scroll to coordinates, { el } to scroll an element into view, or false to skip scrolling; can return a Promise
  • Scroll position of the departing page is automatically saved into history.state before each push(); restored via savedPosition on 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.

DecoratorBeforeAfter
@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

ChangedBeforeAfter
Root app decorator@RouterConfig([...])@Router([...])
Router field decorator@InjectRouter() router!: Router@Router() router!: RouterInstance
Router class (type annotation)RouterRouterInstance
Class exportexport { 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 on RouteDefinition — attach a layout to any route directly; RouterView renders <Layout> around the matched page
  • Layout inheritance via children — when a route has child routes, its component automatically becomes the layout for all of them
  • <RouterOutlet> — new component for use inside layouts (StatelessComponent); reactively renders the current page without needing the children prop
  • @InjectLayout() — field decorator injecting RouterInstance.currentLayout as 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

  • @Lazy inline fix — Lazy(() => import('./Page')) used directly in route definitions now renders correctly. The returned function was missing the __isLazy marker so the router treated it as a plain component
  • @Route co-location — @RouterConfig now accepts classes decorated with @Route directly. The path is read from __routePath automatically — no { path, component } wrapper needed
  • Lazy-loaded pages must use export default class — the loader resolves module.default at runtime

1.0.0

Breaking — full decorator-based API

All functional routing APIs removed:

RemovedReplacement
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.

On this page