getRoutesApi().update
1. Overview
- What it does: Updates configuration of an existing route without rebuilding the route tree. Allows changing
forwardTo, defaultParams, decodeParams, encodeParams, canActivate, canDeactivate. Use null to remove a property.
- When to use:
- Dynamic redirect changes (
forwardTo)
- Updating default parameters
- Replacing encoder/decoder functions
- Changing guards at runtime
- Hot reload of route configuration
2. Signature
import { getRoutesApi } from "@real-router/core/api";
const routesApi = getRoutesApi(router);
routesApi.update(name: string, updates: RouteConfigUpdate<Dependencies>): void
RouteConfigUpdate
import type { RouteConfigUpdate } from "@real-router/types";
interface RouteConfigUpdate<Dependencies> {
forwardTo?: string | ForwardToCallback<Dependencies> | null;
defaultParams?: Params | null;
decodeParams?: ((params: Params) => Params) | null;
encodeParams?: ((params: Params) => Params) | null;
canActivate?: GuardFnFactory<Dependencies> | null;
canDeactivate?: GuardFnFactory<Dependencies> | null;
}
Usage Examples
import { createRouter } from "@real-router/core";
import { getRoutesApi } from "@real-router/core/api";
const router = createRouter(routes);
const routesApi = getRoutesApi(router);
// Adding static forwardTo
routesApi.update("old-page", { forwardTo: "new-page" });
// Adding dynamic forwardTo
routesApi.update("landing", {
forwardTo: (getDependency) =>
getDependency("auth").isAdmin ? "admin" : "user",
});
// Removing forwardTo
routesApi.update("old-page", { forwardTo: null });
// Updating defaultParams
routesApi.update("users", { defaultParams: { page: 1, limit: 10 } });
// Adding decoder for parameters
routesApi.update("users.profile", {
decodeParams: (params) => ({ ...params, id: Number(params.id) }),
});
// Adding encoder for buildPath
routesApi.update("products", {
encodeParams: (params) => ({ ...params, id: `product-${params.id}` }),
});
// Updating activation guard
routesApi.update("admin", {
canActivate: (router, getDependency) => (toState, fromState) => {
return getDependency("auth").isAdmin();
},
});
// Adding deactivation guard
routesApi.update("editor", {
canDeactivate: (router, getDependency) => (toState, fromState) => {
return !getDependency("formState").isDirty();
},
});
// Removing deactivation guard
routesApi.update("editor", { canDeactivate: null });
// Multiple updates at once
routesApi.update("dashboard", {
defaultParams: { tab: "overview" },
canActivate: authGuardFactory,
});
3. Parameters
name (required)
- Type:
string
- Purpose: Route name to update
- Allowed values: Valid route name (regex
^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*$)
- Dot-notation: Supported for nested routes
| Value |
Behavior |
"users" |
Updates users route |
"users.profile" |
Updates nested users.profile |
"nonexistent" |
Throws ReferenceError |
"" |
Throws ReferenceError (root node) |
null / undefined |
Throws TypeError |
updates (required)
- Type:
RouteConfigUpdate<Dependencies>
- Purpose: Object with configuration updates
- Allowed values: Plain object (not
null, not array, not primitive)
Updates Fields
| Field |
Type |
Description |
forwardTo |
string | ForwardToCallback | null |
Route name or callback for redirect |
defaultParams |
Params | null |
Default parameters |
decodeParams |
((params: Params) => Params) | null |
Parameter decoding function |
encodeParams |
((params: Params) => Params) | null |
Parameter encoding function |
canActivate |
GuardFnFactory<Dependencies> | null |
Activation guard factory |
canDeactivate |
GuardFnFactory<Dependencies> | null |
Deactivation guard factory |
Semantics of null
- Passing
null for any field removes the corresponding configuration
- Absent field (or
undefined) -- no-op, existing value is preserved
4. Return Value
5. Side Effects
- Config update: Modifies corresponding entries in
config.forwardMap, config.forwardFnMap, config.defaultParams, config.decoders, config.encoders
- Lifecycle update: Registers/removes
canActivate and canDeactivate handlers
- Forward cache: On
forwardTo change -- revalidation and chain caching
- matchPath/buildPath: Changes apply immediately
What is NOT changed
| Component |
Reason |
| Route tree |
Route structure is not rebuilt |
name |
Route name cannot be changed |
path |
Route path cannot be changed |
children |
Children cannot be modified via update |
6. Possible Errors
| Condition |
Error |
Message |
| Router disposed |
RouterError |
ROUTER_DISPOSED |
name not string |
TypeError |
Route name must be a string |
name whitespace-only |
TypeError |
must not be whitespace-only |
name doesn't match pattern |
TypeError |
does not match pattern |
name > 10000 characters |
TypeError |
exceeds maximum length |
name is empty string |
ReferenceError |
[router.updateRoute] Invalid name: empty string. Cannot update root node. |
| Route doesn't exist |
ReferenceError |
[real-router] updateRoute: route "X" does not exist |
updates is null |
TypeError |
[real-router] updateRoute: updates must be an object, got null |
updates is primitive |
TypeError |
[real-router] updateRoute: updates must be an object, got {type} |
updates is array |
TypeError |
[real-router] updateRoute: updates must be an object, got array |
forwardTo wrong type |
TypeError |
[real-router] updateRoute: forwardTo must be a string, function, or null, got {type} |
defaultParams not object |
TypeError |
[real-router] updateRoute: defaultParams must be an object or null, got {type} |
decodeParams not function |
TypeError |
[real-router] updateRoute: decodeParams must be a function or null, got {type} |
encodeParams not function |
TypeError |
[real-router] updateRoute: encodeParams must be a function or null, got {type} |
decodeParams is async |
TypeError |
[real-router] updateRoute: decodeParams cannot be an async function |
encodeParams is async |
TypeError |
[real-router] updateRoute: encodeParams cannot be an async function |
forwardTo callback is async |
TypeError |
[real-router] updateRoute: forwardTo callback cannot be an async function |
forwardTo target doesn't exist |
Error |
[real-router] updateRoute: forwardTo target "X" does not exist |
forwardTo creates cycle |
Error |
Circular forwardTo: A -> B -> A |
forwardTo chain > 100 |
Error |
forwardTo chain exceeds maximum depth (100): ... |
forwardTo target requires unavailable params |
Error |
[real-router] forwardTo target "X" requires params [Y] that are not available in source "Z" |
Warning (non-blocking)
| Condition |
Action |
| Navigation in progress |
console.error(), update continues |
Error Examples
import { createRouter } from "@real-router/core";
import { getRoutesApi } from "@real-router/core/api";
const router = createRouter(routes);
const routesApi = getRoutesApi(router);
// Validation errors
routesApi.update(123 as any, {}); // TypeError: not a string
routesApi.update("nonexistent", {}); // ReferenceError: does not exist
routesApi.update("route", null as any); // TypeError: must be an object
routesApi.update("route", [] as any); // TypeError: got array
// forwardTo errors
routesApi.update("a", { forwardTo: "nonexistent" }); // Error: target does not exist
routesApi.update("a", { forwardTo: "a" }); // Error: Circular forwardTo
// Transformer function errors
routesApi.update("r", { decodeParams: "string" as any }); // TypeError: must be a function
routesApi.update("r", { decodeParams: async (p) => p }); // TypeError: cannot be async
// Router disposed
router.dispose();
routesApi.update("home", {}); // RouterError: ROUTER_DISPOSED
7. Related Methods
8. Behavior
forwardTo
import { createRouter } from "@real-router/core";
import { getRoutesApi, getPluginApi } from "@real-router/core/api";
const router = createRouter([
{ name: "source", path: "/source" },
{ name: "target", path: "/target" },
{ name: "target2", path: "/target2" },
]);
const routesApi = getRoutesApi(router);
const pluginApi = getPluginApi(router);
// Adding forwardTo
routesApi.update("source", { forwardTo: "target" });
// Updating existing forwardTo
routesApi.update("source", { forwardTo: "target2" });
// Removing forwardTo via null
routesApi.update("source", { forwardTo: null });
// Works with matchPath after update
routesApi.update("source", { forwardTo: "target" });
const state = pluginApi.matchPath("/source");
expect(state?.name).toBe("target"); // Redirect applied
Dynamic forwardTo
// Switching from static to dynamic
routesApi.update("landing", {
forwardTo: (getDependency) => getDependency("config").target,
});
// Switching from dynamic to static
routesApi.update("landing", { forwardTo: "dashboard" });
// Switching to null clears both static and dynamic
routesApi.update("landing", { forwardTo: null });
// Dynamic forwardTo skips target/cycle validation at update time
// (validated at navigation time instead)
routesApi.update("landing", {
forwardTo: (getDep) => getDep("config").target,
});
// No error even if "config" dependency target doesn't exist yet
Cycle Detection
// Direct cycle (A -> A)
expect(() => routesApi.update("a", { forwardTo: "a" })).toThrowError(
/Circular forwardTo/,
);
// Indirect cycle (A -> B -> C -> A)
routesApi.update("a", { forwardTo: "b" });
routesApi.update("b", { forwardTo: "c" });
expect(() => routesApi.update("c", { forwardTo: "a" })).toThrowError(
/Circular forwardTo/,
);
defaultParams
// Adding
routesApi.update("users", { defaultParams: { page: 1, limit: 10 } });
// Updating (full replacement, not merge)
routesApi.update("users", { defaultParams: { page: 2 } });
// limit is NOT preserved!
// Removing via null
routesApi.update("users", { defaultParams: null });
decodeParams
// Adding decoder
const decoder = (params) => ({ ...params, id: Number(params.id) });
routesApi.update("items", { decodeParams: decoder });
// Usage in matchPath
const state = pluginApi.matchPath("/items/123");
expect(state?.params.id).toBe(123); // number, not string
expect(typeof state?.params.id).toBe("number");
// Removing via null
routesApi.update("items", { decodeParams: null });
encodeParams
// Adding encoder
const encoder = (params) => ({ ...params, id: `user-${params.id}` });
routesApi.update("users", { encodeParams: encoder });
// Usage in buildPath
const path = router.buildPath("users", { id: "123" });
expect(path).toBe("/users/user-123");
// Removing via null
routesApi.update("users", { encodeParams: null });
canActivate
// Adding guard
const guardFactory = () => () => true;
routesApi.update("secure", { canActivate: guardFactory });
// Guard will be checked during navigation
// Updating guard (blocks navigation)
const blockingGuard = () => () => false;
routesApi.update("secure", { canActivate: blockingGuard });
// Navigation to 'secure' will now be blocked
try {
await router.navigate("secure");
} catch (err) {
expect(err?.code).toBe("CANNOT_ACTIVATE");
}
// Removing via null
routesApi.update("secure", { canActivate: null });
// Navigation now allowed
canDeactivate
// Adding guard
const guardFactory = () => () => false;
routesApi.update("editor", { canDeactivate: guardFactory });
// Navigation away from 'editor' will now be blocked
await router.navigate("editor");
try {
await router.navigate("home");
} catch (err) {
expect(err?.code).toBe("CANNOT_DEACTIVATE");
}
// Replacing guard
const newGuard = () => () => true;
routesApi.update("editor", { canDeactivate: newGuard });
// Navigation now allowed
// Removing via null
routesApi.update("editor", { canDeactivate: null });
// Guard cleared, navigation allowed
Atomicity
// If validation fails -- nothing is applied
expect(() =>
routesApi.update("route", {
forwardTo: "target",
defaultParams: "invalid" as any, // TypeError
}),
).toThrowError(/defaultParams must be an object/);
// forwardTo was NOT applied
Edge Cases
// Empty object -- no-op
expect(() => routesApi.update("route", {})).not.toThrowError();
// Missing fields -- existing values preserved
routesApi.update("preserve", {});
// defaultParams, decoders, encoders etc. remain unchanged
// Replacement, not merge
routesApi.update("seq", { defaultParams: { a: 1, b: 2 } });
routesApi.update("seq", { defaultParams: { c: 3 } });
// Result: { c: 3 } -- a, b are lost
Guarantees
- Changes are applied atomically (all or nothing)
- On validation error -- config is not changed
null removes property, undefined -- no-op
- Updates apply immediately (affect matchPath/buildPath)
forwardTo chains are validated for cycles and depth
- Async functions for decoder/encoder/forwardTo callback are forbidden
- Route structure (name, path, children) is immutable
Migration from router5
New method. Absent in master branch.
Analog in master
In master version there was no ability to update route configuration after adding. Had to use workarounds:
// Master: direct config access (fragile, undocumented)
router.config.defaultParams["users"] = { page: 1 };
router.config.decoders["users"] = (p) => ({ ...p, id: Number(p.id) });
router.config.encoders["users"] = (p) => ({ ...p, slug: encode(p.slug) });
router.config.forwardMap["old"] = "new";
// Master: for canActivate -- separate method
router.canActivate("users", guardFactory);
// Master: removal -- setting undefined or delete
delete router.config.defaultParams["users"];
router.config.forwardMap["old"] = undefined;
Problems with master approach:
| Problem |
Description |
| No public API |
Direct access to router.config -- internal structure |
| No validation |
forwardTo could reference non-existent route |
| No cycle check |
Could create A -> B -> A cycle |
| No params check |
forwardTo could require unavailable parameters |
| Scattered operations |
Each property -- separate operation |
| No atomicity |
Partial updates on error |
| No forwardMap cache cleanup |
After forwardMap change cache wasn't updated |
| Async functions |
Could set async decoder/encoder (breaks matchPath/buildPath) |
Advantages of New Method
| Aspect |
Master (no method) |
Current (getRoutesApi().update) |
| API |
Internal config access |
Explicit standalone function |
| Route validation |
No |
Existence check |
| forwardTo validation |
No |
Target exists, no cycles, params compatible |
| Type validation |
No |
Full type validation |
| Async detection |
No |
Async decoder/encoder/forwardTo forbidden |
| Atomicity |
No |
All or nothing |
| Property removal |
delete / undefined |
null semantics |
| Cache invalidation |
Manual |
Automatic |
| TypeScript |
No |
Full typing |
Breaking Changes
| Severity |
What Changed |
Was |
Now |
Impact |
| CRITICAL |
API access |
router.config.* |
getRoutesApi(router).update() |
Change all call sites |
| HIGH |
Return value |
None (direct assign) |
void |
No chaining |
| HIGH |
Validation |
None |
Full validation |
Invalid data breaks |
Summary
| Category |
Status |
| Breaking Changes |
None (new method) |
| Public API |
Added |
| Validation |
Full |
| Atomicity |
All or nothing |
| TypeScript |
Full typing |
| Cache management |
Automatic update |