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] -- removed
    • encoders[name] -- removed
    • defaultParams[name] -- removed
    • forwardMap[name] -- removed (source entry)
    • forwardMap[*] -> name -- entries pointing to removed route are cleared
    • forwardFnMap[name] -- removed (dynamic forwardTo callback)
    • routeCustomFields[name] -- removed
  • Lifecycle cleanup:
    • canActivate[name] -- removed
    • canDeactivate[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
  • buildPath throws 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