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