replaceRoutes - greydragon888/real-router GitHub Wiki

getRoutesApi().replace

1. Overview

  • What it does: Atomically replaces all routes in the router. Combines clear + add into a single operation with one tree rebuild, state preservation, and selective guard cleanup.
  • When to use:
    • Hot Module Replacement (HMR) — Webpack or Vite
    • Feature flags / A/B testing — switching route sets by user role
    • Multi-tenant SPA — replacing routes when tenant changes
    • Runtime route replacement without losing state or external guards

2. Signature

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

const routesApi = getRoutesApi(router);

routesApi.replace(routes: Route<Dependencies> | Route<Dependencies>[]): void

Usage Examples

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

const router = createRouter(routes);
const routesApi = getRoutesApi(router);

// Replace all routes
routesApi.replace([
  { name: "home", path: "/" },
  { name: "dashboard", path: "/dashboard" },
]);

// Replace with a single route
routesApi.replace({ name: "maintenance", path: "/" });

// Replace with empty array (clears routes, preserves external guards)
routesApi.replace([]);

3. Parameters

routes (required)

  • Type: Route<Dependencies> | Route<Dependencies>[]
  • Purpose: New route set that completely replaces all existing routes
  • Allowed values: Plain object(s) with required name and path

Route structure is the same as add()name, path, children, canActivate, canDeactivate, forwardTo, defaultParams, decodeParams, encodeParams.

No parent optionreplace() always replaces the entire root-level route set.

4. Return Value

  • Type: void

5. Side Effects

  • Route store: Route tree, definitions, matcher, config are completely replaced
  • State:
    • Preserved if current route exists in the new set (matchPath(currentPath) revalidates)
    • Cleared (undefined) if current route was removed from the new set
  • Lifecycle handlers:
    • Definition guards (from route canActivate/canDeactivate) — cleared and re-registered from new routes
    • External guards (added via addActivateGuard()/addDeactivateGuard()) — preserved
  • Tree rebuilds: Exactly 1 (not 2 like clear() + add())
  • No plugin events: Does not trigger onStop/onStart

Comparison with clear() + add()

Aspect clear() + add() replace()
Tree rebuilds 2 1
State preservation Lost Preserved if route exists
External guards Lost Preserved
Intermediate empty tree Yes No
Plugin onStop/onStart Yes No

6. Possible Errors

Condition Error Message
Router disposed RouterError ROUTER_DISPOSED
Route not object TypeError [router.addRoute] Route must be an object, got {type}
Name not string TypeError [router.addRoute] Route name must be a string
Path not string TypeError [router.addRoute] path must be a string
Whitespace in path Error [router.addRoute] whitespace not allowed
Duplicate in batch Error [router.addRoute] Duplicate route "{name}" in batch

Blocking (error log + no-op)

Condition Action
Navigation in progress console.error(), operation blocked, returns without replacing

Atomicity

Validation happens before any mutations. If new routes fail validation, the old route tree is unchanged:

routesApi.replace([
  { name: "valid", path: "/valid" },
  { name: "valid", path: "/duplicate" }, // duplicate name!
]);
// Error thrown — old routes still intact

7. Related Methods

Method When to use
getRoutesApi().add Add routes without removing existing ones
getRoutesApi().remove Remove a specific route
getRoutesApi().clear Remove all routes (clears external guards too)
getRoutesApi().update Update a single route's configuration
getLifecycleApi() Add external guards (preserved across replace)

8. Behavior

State Revalidation

After replacing routes, the current state is revalidated using matchPath(currentPath):

await router.start("/users/123");

// Route exists in new set — state revalidated via matchPath
routesApi.replace([
  {
    name: "users",
    path: "/users",
    children: [{ name: "profile", path: "/:id" }],
  },
  { name: "settings", path: "/settings" },
]);
router.getState()?.name; // "users.profile" — preserved

// Route removed — state cleared
routesApi.replace([{ name: "settings", path: "/settings" }]);
router.getState(); // undefined — "users.profile" no longer exists

Guard Origin Tracking

replace() distinguishes between definition guards (from route config canActivate/canDeactivate) and external guards (added via getLifecycleApi()):

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

const router = createRouter([
  { name: "admin", path: "/admin", canActivate: roleGuard }, // definition guard
]);
const routesApi = getRoutesApi(router);
const lifecycle = getLifecycleApi(router);

// External guard
lifecycle.addActivateGuard("admin", () => (toState) => isAuthenticated());

// After replace — definition guard re-registered, external guard preserved
routesApi.replace([
  { name: "admin", path: "/admin", canActivate: newRoleGuard },
]);
// Both newRoleGuard (definition) and isAuthenticated (external) are active

HMR — Webpack

// router.ts
import { createRouter } from "@real-router/core";
import { getRoutesApi } from "@real-router/core/api";
import routes from "./routes";

export const router = createRouter(routes);

if (module.hot) {
  module.hot.accept("./routes", () => {
    const updatedRoutes = require("./routes").default;
    getRoutesApi(router).replace(updatedRoutes);
  });
}

HMR — Vite

// router.ts
import { createRouter } from "@real-router/core";
import { getRoutesApi } from "@real-router/core/api";
import routes from "./routes";

export const router = createRouter(routes);

if (import.meta.hot) {
  import.meta.hot.accept("./routes", (newModule) => {
    if (newModule) {
      getRoutesApi(router).replace(newModule.default);
    }
  });
}

HMR — Guards

Definition guards (from canActivate in route config) are re-registered automatically on replace().

External guards (added via addActivateGuard) are preserved on replace(). A separate HMR callback is needed only if the guard logic itself changed:

// guards.ts — external guards
export const adminGuard = () => (toState) => isAdmin();

// HMR for external guards — only needed if guard logic changed
if (import.meta.hot) {
  import.meta.hot.accept("./guards", (newModule) => {
    if (newModule) {
      const lifecycle = getLifecycleApi(router);
      // addActivateGuard overwrites existing (with warning)
      lifecycle.addActivateGuard("admin", newModule.adminGuard);
    }
  });
}

HMR — React

// App.tsx
import { RouterProvider } from "@real-router/react";
import { router } from "./router";

// ErrorBoundary recommended for graceful recovery during HMR
function App() {
  return (
    <ErrorBoundary fallback={<Loading />}>
      <RouterProvider router={router}>
        <Routes />
      </RouterProvider>
    </ErrorBoundary>
  );
}

Router Lifecycle States

// Works on unstarted router
const router = createRouter([]);
routesApi.replace([{ name: "home", path: "/" }]);

// Works on stopped router
router.stop();
routesApi.replace([{ name: "home", path: "/" }]);

// Throws on disposed router
router.dispose();
routesApi.replace([{ name: "home", path: "/" }]);
// RouterError: ROUTER_DISPOSED

Edge Cases

  • Empty array []: Clears all routes and state, preserves external guards
  • Single route (not array): Wraps in array, works as expected
  • Unstarted router: Works — state remains undefined
  • Stopped router: Works — no state revalidation (state is already undefined)
  • Disposed router: Throws RouterError with ROUTER_DISPOSED code
  • During navigation: Silent no-op with logger.error
  • Nested routes: Fully supported via children
  • Validation: Requires @real-router/validation-plugin. External guards are preserved regardless of validation mode

Guarantees

  • Validation happens BEFORE any changes (atomicity)
  • On error, router state is not changed
  • Exactly one tree rebuild per call
  • State is preserved if current route exists in new set
  • External guards are never lost
  • Definition guards are fully replaced
  • No intermediate empty-tree state
  • No plugin onStop/onStart events
  • Blocked during active navigation (error log + no-op)
⚠️ **GitHub.com Fallback** ⚠️