getNavigator - greydragon888/real-router GitHub Wiki

getNavigator

1. Overview

  • What it does: Returns a frozen Navigator interface with 7 safe methods for UI components
  • When to use:
    • For passing router functionality to UI components (React, Vue, Angular)
    • For implementing the principle of least privilege (components get only what they need)
    • For preventing accidental calls to dangerous configuration methods
    • For cleaner TypeScript autocomplete in UI code

2. Signature

import { getNavigator } from "@real-router/core";

getNavigator(router: Router): Navigator

Navigator Interface

interface Navigator {
  navigate: (
    routeName: string,
    routeParams?: Params,
    options?: NavigationOptions,
  ) => Promise<State>;
  getState: () => State | undefined;
  isActiveRoute: (
    name: string,
    params?: Params,
    strictEquality?: boolean,
    ignoreQueryParams?: boolean,
  ) => boolean;
  canNavigateTo: (name: string, params?: Params) => boolean;
  subscribe: (listener: SubscribeFn) => Unsubscribe;
  subscribeLeave: (listener: LeaveFn) => Unsubscribe;
  isLeaveApproved: () => boolean;
}

Navigator Methods

Method Returns Description
navigate Promise Programmatic navigation to routes
getState State Get current route state
isActiveRoute boolean Check if route is currently active
canNavigateTo boolean Check if navigation would be allowed (RBAC)
subscribe Unsubscribe Subscribe to successful route transitions
subscribeLeave(listener) Unsubscribe Subscribe to confirmed route departures
isLeaveApproved() boolean True when deactivation guards have passed

Usage Examples

import { getNavigator } from "@real-router/core";

// Get Navigator instance
const navigator = getNavigator(router);

// Pass to React component
<NavigatorContext.Provider value={navigator}>
  {children}
</NavigatorContext.Provider>

// In component
const nav = useNavigator(); // Navigator type, not full Router

nav.navigate('users.profile', { id: 123 }); // Works
nav.isActiveRoute('users');                  // Works
nav.canNavigateTo('admin');                  // Works (RBAC check)
nav.setDependency('api', api);               // TypeScript Error!

// Destructure methods
const { navigate, canNavigateTo } = getNavigator(router);

if (canNavigateTo('admin')) {
  navigate('admin');
}

// Cached — same router always returns the same instance
const nav1 = getNavigator(router);
const nav2 = getNavigator(router);
console.log(nav1 === nav2); // true

// Subscribe to confirmed route departures
const navigator = getNavigator(router);
const unsub = navigator.subscribeLeave(({ route, nextRoute }) => {
  console.log(`Leaving ${route.name}${nextRoute.name}`);
});

3. Parameters

router (required)

  • Type: Router
  • Purpose: Router instance to extract navigator methods from

4. Return Value

  • Type: Navigator
  • Properties: Frozen object with 7 methods (see table above)
  • Frozen: Object.isFrozen(navigator) === true
  • Cached: Same router always returns the same Navigator instance

5. Side Effects

  • Caching: First call creates and freezes a Navigator for the given router. Subsequent calls return the same instance.
  • No state change: Does not modify router state

6. Possible Errors

This function does not throw errors.

7. Related Methods

All 7 Navigator methods are documented separately:

Method Documentation
navigator.navigate() See navigate
navigator.getState() See getState
navigator.isActiveRoute() See isActiveRoute
navigator.canNavigateTo() See canNavigateTo
navigator.subscribe() See subscribe
navigator.subscribeLeave() See subscribeLeave
navigator.isLeaveApproved() See isLeaveApproved

8. Behavior

Main Scenarios

  • First call: Creates a new Navigator object and freezes it
  • Subsequent calls: Returns the same cached instance (same reference)
  • Frozen object: Cannot be modified (Object.freeze())
  • Method binding: All methods are bound to the router instance (can be destructured safely)
  • Works with cloned routers: getNavigator(cloneRouter(router)) creates a separate cached instance for the clone

Test Examples

import { getNavigator } from "@real-router/core";

// Get Navigator
const nav = getNavigator(router);

// Navigator has 7 methods
expect(typeof nav.navigate).toBe("function");
expect(typeof nav.getState).toBe("function");
expect(typeof nav.isActiveRoute).toBe("function");
expect(typeof nav.canNavigateTo).toBe("function");
expect(typeof nav.subscribe).toBe("function");
expect(typeof nav.subscribeLeave).toBe("function");
expect(typeof nav.isLeaveApproved).toBe("function");

// Navigator is frozen
expect(Object.isFrozen(nav)).toBe(true);

// Same router returns same cached instance
const nav2 = getNavigator(router);
expect(nav).toBe(nav2);

// Methods work correctly
await nav.navigate("users");
expect(nav.getState()?.name).toBe("users");
expect(nav.isActiveRoute("users")).toBe(true);

Edge Cases

  • Before start(): Navigator can be obtained, but getState() returns undefined
  • After stop(): Navigator methods still work (router can be restarted)
  • Cloned router: Each call creates a new Navigator bound to the given router

Design Rationale

Why Standalone Function?

Extracting getNavigator from the Router class provides:

  • Tree-shaking: Applications that don't use Navigator don't pay for it in bundle size
  • Caching: One frozen Navigator per router instance, automatically cleaned up when the router is garbage-collected
  • Testability: Easy to mock and test independently
  • Stable identity: Same router always returns the same Navigator reference — no need for external memoization

Why Only 7 Methods?

The Router interface has 15 methods, plus standalone API functions (getRoutesApi, getDependenciesApi, getLifecycleApi, getPluginApi, cloneRouter). Most are not needed (and potentially dangerous) for UI components:

// Router interface + standalone APIs
router.start("/");                            // Why does UI need this?
router.dispose();                             // Catastrophe with one call
getDependenciesApi(router).set("key", value); // Configuration, not UI
getRoutesApi(router).add([...]);              // Never needed in UI

Navigator provides only what UI needs:

  • Navigation: navigate() — trigger route changes
  • State inspection: getState(), isActiveRoute(), isLeaveApproved() — read current route and departure status
  • Access control: canNavigateTo() — RBAC checks for conditional rendering
  • Reactivity: subscribe() — react to route changes
  • Departure hooks: subscribeLeave() — react to confirmed route departures

Why Frozen?

const nav = getNavigator(router);

// Frozen object prevents accidental mutations
nav.navigate = () => {}; // TypeError in strict mode
nav.customMethod = () => {}; // TypeError in strict mode

React Usage

getNavigator returns the same cached instance for the same router, so no useMemo wrapper is needed:

// Safe — returns cached instance, no extra allocation
const navigator = getNavigator(router);

// RouterProvider passes this to NavigatorContext automatically

Use Cases

React Context

import { getNavigator } from "@real-router/core";

// RouterProvider handles this automatically, but for custom setups:
function App() {
  const navigator = useMemo(() => getNavigator(router), [router]);

  return (
    <NavigatorContext.Provider value={navigator}>
      <Routes />
    </NavigatorContext.Provider>
  );
}

// Hook
function useNavigator() {
  const nav = useContext(NavigatorContext);
  if (!nav) throw new Error("Navigator not found");
  return nav;
}

// Component
function UserProfile() {
  const nav = useNavigator();

  return (
    <button onClick={() => nav.navigate("users.edit", { id: 123 })}>
      Edit Profile
    </button>
  );
}

Vue Composition API

import { getNavigator } from "@real-router/core";

// Provide Navigator
const app = createApp(App);
app.provide("navigator", getNavigator(router));

// Inject in component
export default {
  setup() {
    const nav = inject("navigator");

    const goToProfile = () => {
      nav.navigate("users.profile", { id: 123 });
    };

    return { goToProfile };
  },
};

Angular Service

import { getNavigator } from "@real-router/core";

@Injectable({ providedIn: "root" })
export class NavigatorService {
  private navigator: Navigator;

  constructor() {
    this.navigator = getNavigator(router);
  }

  navigate(name: string, params?: Params) {
    return this.navigator.navigate(name, params);
  }

  canNavigateTo(name: string, params?: Params) {
    return this.navigator.canNavigateTo(name, params);
  }
}

RBAC Menu Filtering

import { getNavigator } from "@real-router/core";

function Navigation() {
  const nav = getNavigator(router);

  const menuItems = [
    { route: "home", label: "Home" },
    { route: "admin", label: "Admin Panel" },
    { route: "users", label: "Users" },
  ].filter(item => nav.canNavigateTo(item.route));

  return (
    <nav>
      {menuItems.map(item => (
        <button key={item.route} onClick={() => nav.navigate(item.route)}>
          {item.label}
        </button>
      ))}
    </nav>
  );
}

What's NOT Included

The following Router methods and standalone APIs are intentionally excluded from Navigator:

Method / API Reason
navigateToDefault Rare use case. Use navigate('home') explicitly.
getPreviousState Rare use case. Track in subscribe() if needed.
buildPath Rare use case. Use navigate() for navigation.
isActive UI renders after router start, check not needed
start / stop / dispose Lifecycle management, not UI concern
usePlugin Plugin registration, not UI concern
getRoutesApi(router) Route CRUD, never needed in UI
getDependenciesApi(router) Dependency management, not UI concern
getLifecycleApi(router) Guard management, not UI concern
getPluginApi(router) Plugin API (addEventListener, etc.), not UI concern

Benefits

  1. Safety — UI cannot break router configuration
  2. Simplicity — 7 methods instead of 15+ (Router) plus standalone APIs
  3. Clarity — Immediately clear what UI can do
  4. Better TypeScript DX — Clean autocomplete
  5. Easier testing — Simple interface to mock
  6. Principle of least privilege — Components only get what they need
  7. RBAC supportcanNavigateTo() enables permission-based UI rendering
  8. Tree-shakeable — Standalone function, not bundled if unused

Migration from router.getNavigator()

The getNavigator() method was extracted from the Router class into a standalone function.

// Before
const navigator = router.getNavigator();

// After
import { getNavigator } from "@real-router/core";
const navigator = getNavigator(router);

Key differences:

Aspect Before (method) After (standalone)
Call syntax router.getNavigator() getNavigator(router)
Caching Cached on router instance Cached per router instance
Tree-shaking Always in bundle Only if imported
React usage No useMemo needed No useMemo needed (cached)

Notes

  • Navigator is frozen — cannot be modified
  • Navigator is cached — one instance per router, stable identity across calls
  • Navigator methods are bound — can be destructured safely
  • Navigator is UI-safe — no dangerous configuration methods
  • Navigator is framework-agnostic — works with React, Vue, Angular, vanilla JS
  • subscribeLeave() fires during the LEAVE_APPROVED FSM phase, after deactivation guards pass
  • isLeaveApproved() returns true only while the router is in the LEAVE_APPROVED phase

See Also

⚠️ **GitHub.com Fallback** ⚠️