replaceRoutes - greydragon888/real-router GitHub Wiki
-
What it does: Atomically replaces all routes in the router. Combines
clear + addinto 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
import { getRoutesApi } from "@real-router/core/api";
const routesApi = getRoutesApi(router);
routesApi.replace(routes: Route<Dependencies> | Route<Dependencies>[]): voidimport { 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([]);-
Type:
Route<Dependencies> | Route<Dependencies>[] - Purpose: New route set that completely replaces all existing routes
-
Allowed values: Plain object(s) with required
nameandpath
Route structure is the same as add() — name, path, children, canActivate, canDeactivate, forwardTo, defaultParams, decodeParams, encodeParams.
No parent option — replace() always replaces the entire root-level route set.
-
Type:
void
- 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
-
Preserved if current route exists in 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
-
Definition guards (from route
-
Tree rebuilds: Exactly 1 (not 2 like
clear() + add()) -
No plugin events: Does not trigger
onStop/onStart
| 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 |
| 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 |
| Condition | Action |
|---|---|
| Navigation in progress |
console.error(), operation blocked, returns without replacing |
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| 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) |
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 existsreplace() 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// 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);
});
}// 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);
}
});
}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);
}
});
}// 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>
);
}// 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-
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
RouterErrorwithROUTER_DISPOSEDcode -
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
- 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/onStartevents - Blocked during active navigation (error log + no-op)