-
-
Notifications
You must be signed in to change notification settings - Fork 1
Routes Mutation Event
-
What it does: Notifies you whenever the route tree is structurally mutated through
getRoutesApi(router)—add,remove,update,replace, orclear. It is the route-tree counterpart tosubscribe(which fires on navigation transitions). -
When to use:
- DevTools / inspectors — visualize the live route tree as it changes.
- Microfrontend coordinators — one shell observes routes registered by independently-loaded modules.
-
File-based routing watch mode — a watcher drives
add/update/remove, and consumers react without each re-implementing the diff. - Plugins with route-keyed caches — invalidate compiled/derived state when routes change (see Plugin Architecture).
-
When not to use: app-level UI logic. Tree mutations are an infrastructural concern. If you want "re-render the menu when auth changes," subscribe to your auth store (and to
subscribefor navigation) — not to the tree. There is deliberately norouter.subscribeTree()facade.
import { getRoutesApi } from "@real-router/core/api";
const routesApi = getRoutesApi(router);
routesApi.subscribeChanges(
handler: (event: TreeChangedEvent) => void
): Unsubscribe;import type { TreeChangedEvent } from "@real-router/core";
type TreeChangedEvent =
| { op: "add"; added: readonly Route[]; parent?: string }
| { op: "remove"; name: string; removedSubtree: readonly Route[] }
| { op: "update"; name: string; patch: Readonly<TreeStructuralPatch> }
| { op: "replace"; removed: readonly Route[]; added: readonly Route[] }
| { op: "clear"; removed: readonly Route[] };
// Structural fields only — guard fields are intentionally excluded.
type TreeStructuralPatch = Pick<
RouteConfigUpdate,
"forwardTo" | "defaultParams" | "encodeParams" | "decodeParams"
>;- Route arrays are flat and use full dotted names (
"users.profile"), with descendants included. -
parentis present onaddonly when you added under a parent (add(routes, { parent })).
const routes = getRoutesApi(router);
const unsubscribe = routes.subscribeChanges((event) => {
switch (event.op) {
case "add":
event.added.forEach((r) => console.log("added", r.name));
break;
case "remove":
event.removedSubtree.forEach((r) => cache.delete(r.name));
break;
case "update":
if (event.patch.defaultParams) revalidate(event.name);
break;
case "replace":
event.removed.forEach((r) => cache.delete(r.name));
event.added.forEach((r) => register(r));
break;
case "clear":
cache.clear();
break;
default:
// exhaustiveness guard — do NOT rely on Object.keys(event) or array order
break;
}
});
// later
unsubscribe();function attachTreeInspector(router: Router): () => void {
return getRoutesApi(router).subscribeChanges((event) => {
devtools.send({ type: "route-tree", op: event.op, event });
});
}// Shell observes routes registered by lazily-loaded modules — without knowing
// which module added them.
getRoutesApi(shellRouter).subscribeChanges((event) => {
if (event.op === "add") {
event.added.forEach((r) => registry.publish(r.name));
}
});
// Module B, loaded later, registers its own routes:
getRoutesApi(shellRouter).add(moduleBRoutes, { parent: "modules" });The router is the first argument of every plugin factory:
const myPlugin: PluginFactory = (router) => {
const cache = new Map<string, Compiled>();
const unsubscribe = getRoutesApi(router).subscribeChanges((event) => {
switch (event.op) {
case "remove":
event.removedSubtree.forEach((r) => cache.delete(r.name));
break;
case "clear":
cache.clear();
break;
// add/update: recompile lazily on next access
}
});
return { teardown: () => unsubscribe() };
};This mirrors how @real-router/preload-plugin and @real-router/lifecycle-plugin evict route-keyed compiled caches, and how @real-router/search-schema-plugin re-validates defaultParams on add/update/replace. See Plugin Architecture and the packages/core/CLAUDE.md section "Recommended pattern: declarative reactive cache invalidation."
| Property | Behavior |
|---|---|
| Timing | Post-commit — the handler observes the new tree via get() / has(). For replace, it fires after the tree swap but before state revalidation (new tree, still-old state). |
| Atomicity | One CRUD call = one event. add([r1, r2, r3]) emits one event with three entries, not three. |
update filter |
Emits only when the patch contains a structural field (forwardTo / defaultParams / encodeParams / decodeParams). Guard-only (canActivate / canDeactivate) and empty patches are silent. |
| Fire-and-forget | The handler cannot cancel the mutation; any returned Promise is ignored. |
| Re-entrancy | Calling add/remove/… from inside a handler is allowed and emits a nested event synchronously. Runaway recursion past depth 5 throws RecursionDepthError (re-exported from @real-router/core) to the CRUD caller. |
| Error isolation | A throwing handler is reported and does not stop other handlers or re-throw to the caller — except RecursionDepthError, which propagates. |
| Duplicates | Lenient — each subscribeChanges call is an independent subscription with its own unsubscribe. |
| Clone isolation | A cloned router (cloneRouter) has an independent channel; mutations never cross the clone boundary. |
| dispose |
router.dispose() releases all subscriptions before tearing down routes — no event fires during disposal. |
Forward-compatibility: write
switch (event.op)with an explicitdefault. Do not depend onObject.keys(event), array ordering, or the absence of future fields.
-
TreeChangedEvent(and itsopvariants) are exported from@real-router/coreand@real-router/types. - The channel is internal-only:
TREE_CHANGEDis not part of the publicevents.*registry, theEventNameunion, or thePlugininterface.addEventListener(events.TREE_CHANGED, …)does not exist —subscribeChangesis the only entry point. - Payload routes carry standard route fields (name, path,
forwardTo,defaultParams, encode/decode, guards) but not custom fields — consistent withgetRoute. Read custom fields viagetRouteConfig(name)when needed. -
Treat payloads as read-only — immutability is shallow. The route object (and the
updatepatch) is frozen, but nested config is shared by reference with the live router config (event.added[0].defaultParamsis the same object the router reads on every navigation, same asgetRoute, and it is not frozen). Mutating a nested field —event.added[0].defaultParams.x = …, anencodeParams/guard closure — corrupts the router's config. If you need to keep or transform payload data, copy it first.
Related pages: add (addRoute) · remove (removeRoute) · update (updateRoute) · replace (replaceRoutes) · clear (clearRoutes) · subscribe · extendRouter · Plugin Architecture
- View-Agnostic Design
- Core Concepts
- Navigation Lifecycle
- Guards
- Plugin Architecture
- Hash Fragment Support
- Accessibility (A11y)
- Server-Side Rendering
- Data Loading
- Streaming SSR
- SSR Cancellation
- RSC Integration
- Testing
- Glossary
Tree-shakeable functions — import only what you need.
- add (addRoute)
- remove (removeRoute)
- replace (replaceRoutes)
- clear (clearRoutes)
- get (getRoute)
- has (hasRoute)
- update (updateRoute)
- subscribeChanges (Routes Mutation Event)
- get (getDependency)
- getAll (getDependencies)
- set (setDependency)
- setAll (setDependencies)
- has (hasDependency)
- remove (removeDependency)
- reset (resetDependencies)
For plugin authors, not for general use.
- makeState
- buildState
- buildNavigationState
- forwardState
- getForwardState
- setForwardState
- matchPath
- setRootPath
- getRootPath
- navigateToState
- addEventListener
- getRouteConfig
- getOptions
- addBuildPathInterceptor
- extendRouter
- getTree
- React Integration Guide
- Preact Integration Guide
- Solid Integration Guide
- Vue Integration Guide
- Svelte Integration Guide
- Ink (Terminal UI) Integration Guide
- Desktop (Electron, Tauri) Integration Guide
- useRouter
- useRoute
- useRouteNode
- useNavigator
- useRouteUtils
- useRouterTransition
- useRouteExit
- useRouteEnter
- useRouteStore (Solid only)
- useRouteNodeStore (Solid only)
SSR-feature subpath —
@real-router/{adapter}/ssr. Symmetric across React/Preact/Solid/Vue/Svelte.
- Lazy (Svelte only — dynamic component import)
- Await — read deferred SSR data by key
- Streamed — Suspense-style boundary
- ClientOnly — server fallback → client children swap
- ServerOnly — server children, removed after hydration
- HttpStatusCode — render-time HTTP status declaration
-
HttpStatusProvider — provides sink to descendant
<HttpStatusCode> - useDeferred — read deferred Promise by key