removeRoute - greydragon888/real-router GitHub Wiki
getRoutesApi().remove
1. Overview
- What it does: Removes a route and all its descendant routes from the router. Clears all related configurations (lifecycle handlers, decoders, encoders, defaultParams, forwardMap, forwardFnMap, custom fields).
- When to use:
- Dynamic route removal at runtime
- Cleaning up routes when exiting a module or plugin
- Replacing a route (remove + add)
- Code splitting: removing unused routes
2. Signature
import { getRoutesApi } from "@real-router/core/api";
const routesApi = getRoutesApi(router);
routesApi.remove(name: string): void
Usage Examples
import { createRouter } from "@real-router/core";
import { getRoutesApi } from "@real-router/core/api";
const router = createRouter(routes);
const routesApi = getRoutesApi(router);
// Removing a simple route
routesApi.remove("users");
// Removing a nested route
routesApi.remove("users.profile");
// Removing parent removes all children too
routesApi.add({
name: "settings",
path: "/settings",
children: [
{ name: "profile", path: "/profile" },
{ name: "security", path: "/security" },
],
});
routesApi.remove("settings");
// Removes: settings, settings.profile, settings.security
// Replacing a route
routesApi.remove("old-route");
routesApi.add({ name: "old-route", path: "/new-path" });
3. Parameters
name (required)
- Type:
string - Purpose: Name of the route to remove
- Allowed values: Valid route name (regex
^[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*$) - Dot-notation: Supported for nested routes
| Value | Behavior |
|---|---|
"users" |
Removes users and all children |
"users.profile" |
Removes only users.profile |
"nonexistent" |
Warning to console, no-op |
null / undefined |
TypeError |
4. Return Value
- Type:
void - Does not return a value
5. Side Effects
- Route tree: Route tree is rebuilt after removal
- Config cleanup:
decoders[name]-- removedencoders[name]-- removeddefaultParams[name]-- removedforwardMap[name]-- removed (source entry)forwardMap[*] -> name-- entries pointing to removed route are clearedforwardFnMap[name]-- removed (dynamic forwardTo callback)routeCustomFields[name]-- removed
- Lifecycle cleanup:
canActivate[name]-- removedcanDeactivate[name]-- removed
- Children cascade: All descendants (
name.*) are cleaned up with the same rules - Logging: Warning if route not found or route is currently active
6. Possible Errors
| Condition | Error | Message |
|---|---|---|
name is not a string |
TypeError |
Route name must be a string |
name is whitespace-only |
TypeError |
must not be whitespace-only |
name doesn't match pattern |
TypeError |
does not match pattern |
name exceeds 10000 characters |
TypeError |
exceeds maximum length |
| Unicode characters | TypeError |
does not match pattern |
| Router is disposed | RouterError |
ROUTER_DISPOSED |
Behavior WITHOUT error (warning + no-op)
| Condition | Action |
|---|---|
| Route doesn't exist | console.warn(), returns |
| Route is active | console.warn(), skips removal |
| Parent of active route | console.warn(), skips removal |
| Navigation in progress | console.warn(), removal proceeds |
Examples
import { getRoutesApi } from "@real-router/core/api";
const routesApi = getRoutesApi(router);
// Error: invalid type
routesApi.remove(null); // TypeError
routesApi.remove(123); // TypeError
// Warning: route doesn't exist
routesApi.remove("nonexistent");
// Warning: Route "nonexistent" not found. No changes made.
// Warning: route is active
await router.navigate("dashboard");
routesApi.remove("dashboard");
// Warning: Cannot remove route "dashboard" -- it is currently active. Navigate away first.
// Warning: parent of active route
await router.navigate("parent.child");
routesApi.remove("parent");
// Warning: Cannot remove route "parent" -- it is currently active (current: "parent.child"). Navigate away first.
// Error: router is disposed
router.dispose();
routesApi.remove("home"); // throws RouterError(ROUTER_DISPOSED)
7. Related Methods
| Method | When to use |
|---|---|
| getRoutesApi().add | Adding routes |
| getRoutesApi().clear | Removing ALL routes |
| getRoutesApi().has | Check existence before removal |
| getRoutesApi().get | Get route information |
| getRoutesApi().update | Update route config without removing |
| navigate() | Navigate to another route before removal |
8. Behavior
Complete Removal
Removed routes are no longer matched by matchPath and buildPath throws for them. Routes can be re-added after removal.
import { createRouter } from "@real-router/core";
import { getRoutesApi, getPluginApi } from "@real-router/core/api";
const router = createRouter([
{ name: "home", path: "/home" },
{ name: "temporary", path: "/temporary" },
]);
const routesApi = getRoutesApi(router);
const pluginApi = getPluginApi(router);
// Route matches before removal
expect(pluginApi.matchPath("/temporary")?.name).toBe("temporary");
// After removal
routesApi.remove("temporary");
expect(pluginApi.matchPath("/temporary")).toBeUndefined();
expect(() => router.buildPath("temporary")).toThrowError(/not defined/);
// Re-adding with new path after removal
routesApi.add({ name: "reusable", path: "/old-path" });
routesApi.remove("reusable");
routesApi.add({ name: "reusable", path: "/new-path" });
expect(pluginApi.matchPath("/new-path")?.name).toBe("reusable");
expect(pluginApi.matchPath("/old-path")).toBeUndefined();
Children Cascade
Removing a parent route removes all its descendant routes. Removing a specific child leaves the parent and siblings intact.
routesApi.add({
name: "parent",
path: "/parent",
children: [
{ name: "child1", path: "/child1" },
{ name: "child2", path: "/child2" },
],
});
routesApi.remove("parent");
expect(pluginApi.matchPath("/parent")).toBeUndefined();
expect(pluginApi.matchPath("/parent/child1")).toBeUndefined();
expect(pluginApi.matchPath("/parent/child2")).toBeUndefined();
// Removing specific child leaves parent and siblings
routesApi.add({
name: "category",
path: "/category",
children: [
{ name: "keep", path: "/keep" },
{ name: "remove", path: "/remove" },
],
});
routesApi.remove("category.remove");
expect(pluginApi.matchPath("/category")?.name).toBe("category"); // remained
expect(pluginApi.matchPath("/category/keep")?.name).toBe("category.keep"); // remained
expect(pluginApi.matchPath("/category/remove")).toBeUndefined(); // removed
Lifecycle Cleanup
Guards registered on a route (via canActivate or canDeactivate) are cleaned up when the route is removed. Guards on child routes are also cleaned up when a parent is removed.
import { getRoutesApi, getLifecycleApi } from "@real-router/core/api";
const routesApi = getRoutesApi(router);
const lifecycleApi = getLifecycleApi(router);
// canActivate guard is cleaned up
routesApi.add({
name: "protected",
path: "/protected",
canActivate: (router) => (toState, fromState) => false, // blocks navigation
});
await router.start("/home");
await expect(router.navigate("protected")).rejects.toMatchObject({
code: "CANNOT_ACTIVATE",
});
routesApi.remove("protected");
expect(routesApi.has("protected")).toBe(false);
// canDeactivate guard is cleaned up
lifecycleApi.addDeactivateGuard("editor", () => () => false);
routesApi.remove("editor");
expect(routesApi.has("editor")).toBe(false);
// Children lifecycle handlers are cleaned up when parent is removed
lifecycleApi.addDeactivateGuard("area.page", () => () => false);
routesApi.remove("area");
expect(routesApi.has("area.page")).toBe(false);
Config Cleanup
All configuration entries associated with the removed route are cleaned up, including entries that reference the removed route as a forwardTo target.
// decoders are cleaned up
routesApi.add({
name: "encoded",
path: "/encoded/:id",
decodeParams: (p) => ({ ...p, id: Number(p.id) }),
});
routesApi.remove("encoded");
// decoder for "encoded" no longer exists
// encoders and defaultParams are cleaned up similarly
// forwardMap target references are cleaned up
routesApi.add({ name: "newDashboard", path: "/new-dashboard" });
routesApi.add({
name: "oldDashboard",
path: "/old-dashboard",
forwardTo: "newDashboard",
});
// oldDashboard forwards to newDashboard
routesApi.remove("newDashboard");
// forwardMap entry for oldDashboard -> newDashboard is also cleared
Active Route Protection
The router prevents removal of the currently active route (or any parent of the active route) to avoid inconsistent state. A warning is logged and the removal is silently skipped.
await router.start("/dashboard");
// Active route is protected
routesApi.remove("dashboard");
// Warning: Cannot remove route "dashboard" -- it is currently active.
expect(routesApi.has("dashboard")).toBe(true); // not removed
// Parent of active route is protected
await router.navigate("parentRoute.childRoute");
routesApi.remove("parentRoute");
// Warning: Cannot remove route "parentRoute" -- it is currently active (current: "parentRoute.childRoute").
expect(routesApi.has("parentRoute")).toBe(true); // not removed
// Navigate away first, then remove
await router.navigate("other");
routesApi.remove("dashboard"); // now works
expect(routesApi.has("dashboard")).toBe(false);
During Navigation
If a navigation is in progress, the route can still be removed, but a warning is logged about potential unexpected behavior.
Edge Cases
- Repeated removal: Warning ("not found"), no-op
- Non-existent child: Warning, no-op, parent and siblings remain
- Deep nesting (15 levels): Works correctly
- Prototype pollution names:
__proto__,constructor,prototype-- warning, no-op
Guarantees
- Removed route does not match via
matchPath buildPaththrows error for removed route- All related configurations are cleaned up (decoders, encoders, defaultParams, forwardMap, forwardFnMap, custom fields)
- All lifecycle handlers are cleaned up (canActivate, canDeactivate)
- Children are removed when parent is removed
- Active route is not removed (protection from inconsistent state)
- During navigation -- warning logged, but removal proceeds
RouterError(ROUTER_DISPOSED)if called on a disposed router
9. Migration from router5
New method. There was no route removal API in router5.
Analog in router5
In router5 there was no ability to remove routes. The only option was to recreate the router:
// router5: recreating router to "remove" a route
const routesWithoutDeleted = routes.filter((r) => r.name !== "toDelete");
const newRouter = createRouter(routesWithoutDeleted, options, dependencies);
// Lost: state, subscribers, plugins
Problems with the old approach:
| Problem | Description |
|---|---|
| No public API | Impossible to remove route without recreation |
| State loss | Recreating router loses current state |
| Subscriber loss | All listeners need re-registration |
| Plugin loss | Plugins need reconnection |
| Memory leaks | Old handlers may remain in memory |
| No cascade delete | Impossible to automatically remove children |
| No cleanup | decoders/encoders/defaultParams remain |
Advantages of getRoutesApi().remove
| Aspect | router5 (no method) | real-router (getRoutesApi().remove) |
|---|---|---|
| API | Absent | Explicit public method |
| State | Lost on recreation | Preserved |
| Subscribers | Lost on recreation | Preserved |
| Plugins | Lost on recreation | Preserved |
| Cascade delete | Manual | Automatic |
| Config cleanup | No | Full cleanup |
| Lifecycle cleanup | No | Full cleanup |
| Active route protection | No | Warning + blocking |
| forwardTo cleanup | No | Reference cleanup |