Skip to content

Routes Mutation Event

olegivanov edited this page Jun 8, 2026 · 1 revision

Routes Mutation Event (subscribeChanges)

1. Overview

  • What it does: Notifies you whenever the route tree is structurally mutated through getRoutesApi(router)add, remove, update, replace, or clear. It is the route-tree counterpart to subscribe (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 subscribe for navigation) — not to the tree. There is deliberately no router.subscribeTree() facade.

2. Signature

import { getRoutesApi } from "@real-router/core/api";

const routesApi = getRoutesApi(router);

routesApi.subscribeChanges(
  handler: (event: TreeChangedEvent) => void
): Unsubscribe;

Payload — TreeChangedEvent (discriminated union by op)

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.
  • parent is present on add only when you added under a parent (add(routes, { parent })).

3. Usage

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();

Use case: DevTools tree mirror

function attachTreeInspector(router: Router): () => void {
  return getRoutesApi(router).subscribeChanges((event) => {
    devtools.send({ type: "route-tree", op: event.op, event });
  });
}

Use case: microfrontend route registry

// 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" });

Use case: plugin cache invalidation (inside a PluginFactory)

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

4. Guarantees

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 explicit default. Do not depend on Object.keys(event), array ordering, or the absence of future fields.

5. Notes

  • TreeChangedEvent (and its op variants) are exported from @real-router/core and @real-router/types.
  • The channel is internal-only: TREE_CHANGED is not part of the public events.* registry, the EventName union, or the Plugin interface. addEventListener(events.TREE_CHANGED, …) does not exist — subscribeChanges is the only entry point.
  • Payload routes carry standard route fields (name, path, forwardTo, defaultParams, encode/decode, guards) but not custom fields — consistent with getRoute. Read custom fields via getRouteConfig(name) when needed.
  • Treat payloads as read-only — immutability is shallow. The route object (and the update patch) is frozen, but nested config is shared by reference with the live router config (event.added[0].defaultParams is the same object the router reads on every navigation, same as getRoute, and it is not frozen). Mutating a nested field — event.added[0].defaultParams.x = …, an encodeParams/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

Navigation

Home


Concepts


Getting Started


Router Methods

Lifecycle

Navigation

State

URL & Path

Events


Standalone API

Tree-shakeable functions — import only what you need.

Routes — getRoutesApi(router)

Dependencies — getDependenciesApi(router)

Guards — getLifecycleApi(router)

Plugin Infrastructure — getPluginApi(router)

For plugin authors, not for general use.

SSR / SSG


React / Preact / Solid / Vue / Svelte Integration

Provider

Hooks

Components

SSR Components & Hooks

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

DOM Utilities

Patterns


Subscription Layer (@real-router/sources)


Reactive Streams (@real-router/rx)


Plugins

Browser Plugin

Navigation Plugin

Hash Plugin

Memory Plugin

Lifecycle Plugin

Preload Plugin

Logger Plugin

Persistent Params

SSR Data

RSC Server

Validation

Search Schema

Utilities


Reference

Types

Error Codes

Clone this wiki locally