clone - greydragon888/real-router GitHub Wiki
cloneRouter
Overview
cloneRouter() creates an independent copy of a router with the same route tree, configuration, lifecycle guards, and plugins. The primary use case is server-side rendering (SSR), where each incoming request needs its own router instance with request-specific dependencies (e.g., auth tokens, request context).
It is also useful for:
- Testing -- fresh router with mock dependencies
- Microfrontends -- multiple isolated router instances
Signature
import { createRouter } from "@real-router/core";
import { cloneRouter } from "@real-router/core/api";
const router = createRouter<Dependencies>(routes, options, dependencies);
const clonedRouter = cloneRouter(router, newDependencies?);
Usage Examples
// SSR: isolated router per request
function handleRequest(req: Request) {
const requestRouter = cloneRouter(router, {
token: req.headers.authorization,
});
await requestRouter.start(req.url);
// render...
}
// Testing: fresh router with mock dependencies
const testRouter = cloneRouter(router, { api: mockApi });
// Clone without new dependencies (inherits original's dependencies)
const clonedRouter = cloneRouter(router);
Parameters
router
- Type:
Router<Dependencies> - Required: Yes
- Purpose: The source router to clone
dependencies (optional)
- Type:
Dependencies - Default:
undefined(original router's dependencies are preserved as-is) - Purpose: Additional dependencies to merge into the clone. Clone dependencies override originals.
- Validation: Must be a plain object (no arrays, class instances, Maps). Getters are not allowed.
| Value | Result |
|---|---|
undefined / not passed |
Clone inherits original's dependencies (shallow copy) |
{ token: "abc" } |
Merged on top of original's dependencies (clone overrides) |
Return Value
- Type:
Router<Dependencies>-- a new, independent router instance - State: Not active (
isActive() === false), no active state (getState() === undefined)
What Gets Cloned
| Component | Cloned | How |
|---|---|---|
| Route tree | Yes | Rebuilt from route definitions (not shared) |
| Options | Yes | Deep copy |
| Dependencies | Yes | Shallow copy, merged with provided dependencies |
| decoders | Yes | Shallow copy (function references) |
| encoders | Yes | Shallow copy (function references) |
| defaultParams | Yes | Deep copy (structuredClone) |
| forwardMap | Yes | Shallow copy |
| forwardFnMap | Yes | Shallow copy (function references) |
| Resolved forward map | Yes | Shallow copy |
| Route custom fields | Yes | Shallow copy |
| canActivate guards | Yes | Re-registered via getLifecycleApi |
| canDeactivate guards | Yes | Re-registered via getLifecycleApi |
| Plugin factories | Yes | Re-registered via usePlugin |
What Does NOT Get Cloned
| Component | Reason |
|---|---|
| Current state | Clone starts "clean" |
isActive status |
Clone starts as not active |
| Subscribers/listeners | Event isolation |
Possible Errors
| Condition | Error | Description |
|---|---|---|
| Source router is disposed | RouterError(DISPOSED) |
Cannot clone a disposed router |
dependencies is not plain obj |
TypeError |
Arrays, class instances, Maps, etc. are rejected |
Getter in dependencies |
TypeError |
Getters are not allowed in dependencies |
Functions in defaultParams |
DataCloneError |
structuredClone cannot clone functions |
// Disposed router
router.dispose();
cloneRouter(router); // RouterError: DISPOSED
// Invalid dependencies
cloneRouter(router, [1, 2, 3]); // TypeError: expected plain object
cloneRouter(router, new Map()); // TypeError: expected plain object
// Getter in dependencies
const deps = {};
Object.defineProperty(deps, "token", { get: () => "secret" });
cloneRouter(router, deps); // TypeError: Getters not allowed
Related
| Function | Relationship |
|---|---|
createRouter |
Creates the source router to clone |
getDependenciesApi |
Access dependencies on the cloned router |
getLifecycleApi |
Manage guards on the cloned router |
Behavior
Route Tree is Shared
The route tree is immutable (deeply frozen), so it is shared between original and clone rather than rebuilt. This makes cloning fast.
import { createRouter } from "@real-router/core";
import { cloneRouter } from "@real-router/core/api";
const router = createRouter(routes, options);
const clone = cloneRouter(router);
// Routes work identically
router.buildPath("home") === clone.buildPath("home"); // true
router.buildPath("users.view", { id: "1" }) ===
clone.buildPath("users.view", { id: "1" }); // true
Clone Independence
Each clone is independent -- navigation on one does not affect the other.
import { createRouter } from "@real-router/core";
import { cloneRouter, getDependenciesApi } from "@real-router/core/api";
// Returns new instance
const router = createRouter(routes, options);
const clone = cloneRouter(router);
clone !== router; // true
// Dependencies are merged (clone overrides originals)
const router = createRouter(routes, {}, { token: "original", baseUrl: "/api" });
const clone = cloneRouter(router, { token: "cloned" });
const deps = getDependenciesApi(clone);
deps.get("token"); // "cloned" -- overridden
deps.get("baseUrl"); // "/api" -- inherited from original
Works Without Dependencies
import { createRouter } from "@real-router/core";
import { cloneRouter, getRoutesApi } from "@real-router/core/api";
const router = createRouter([{ name: "home", path: "/home" }]);
const clone = cloneRouter(router);
getRoutesApi(clone).has("home"); // true
Lifecycle Guards are Re-registered
Guards registered on the source router are re-registered on the clone. Adding guards to the clone does not affect the original.
import { cloneRouter, getLifecycleApi } from "@real-router/core/api";
// Guards from source are re-registered on clone
const lifecycle = getLifecycleApi(router);
lifecycle.addActivateGuard("admin", guardFactory);
const clone = cloneRouter(router);
// clone has the "admin" activate guard
// Adding to clone doesn't affect original
getLifecycleApi(clone).addActivateGuard("users", newGuardFactory);
// "users" guard only exists on clone
Plugin Factories are Re-registered
const clone = cloneRouter(router);
// All plugin factories from original are re-registered on clone
// Adding plugins to clone doesn't affect original
clone.usePlugin(newPlugin);
Migration
The API has changed from a router method to a standalone function:
| Aspect | Old (router5 / early versions) | New |
|---|---|---|
| Call | router.clone(deps?) |
cloneRouter(router, deps?) |
| Import | Method on router instance | import { cloneRouter } from "@real-router/core/api" |
| Deps | Original deps NOT copied automatically | Original deps shallow-copied, merged with new |
| Config | Some versions shared config by reference | Config copied independently (isolated) |
Migration Examples
// Old
const clone = router.clone({ api: mockApi });
// New
import { cloneRouter } from "@real-router/core/api";
const clone = cloneRouter(router, { api: mockApi });
// Old -- dependencies not copied automatically
const clone = router.clone(router.getDependencies());
// New -- original dependencies are always inherited; pass overrides only
import { cloneRouter } from "@real-router/core/api";
const clone = cloneRouter(router); // inherits all original deps
const clone2 = cloneRouter(router, { token: "override" }); // merges