getNavigator - greydragon888/real-router GitHub Wiki
- 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
import { getNavigator } from "@real-router/core";
getNavigator(router: Router): Navigatorinterface 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;
}| 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 |
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}`);
});-
Type:
Router - Purpose: Router instance to extract navigator methods from
-
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
- 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
This function does not throw errors.
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 |
- 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
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);-
Before
start(): Navigator can be obtained, butgetState()returnsundefined -
After
stop(): Navigator methods still work (router can be restarted) - Cloned router: Each call creates a new Navigator bound to the given router
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
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 UINavigator 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
const nav = getNavigator(router);
// Frozen object prevents accidental mutations
nav.navigate = () => {}; // TypeError in strict mode
nav.customMethod = () => {}; // TypeError in strict modegetNavigator 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 automaticallyimport { 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>
);
}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 };
},
};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);
}
}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>
);
}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 |
- Safety — UI cannot break router configuration
- Simplicity — 7 methods instead of 15+ (Router) plus standalone APIs
- Clarity — Immediately clear what UI can do
- Better TypeScript DX — Clean autocomplete
- Easier testing — Simple interface to mock
- Principle of least privilege — Components only get what they need
-
RBAC support —
canNavigateTo()enables permission-based UI rendering - Tree-shakeable — Standalone function, not bundled if unused
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);| 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) |
- 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 theLEAVE_APPROVEDFSM phase, after deactivation guards pass -
isLeaveApproved()returnstrueonly while the router is in theLEAVE_APPROVEDphase
- RFC-3: getNavigator() Method — Original design rationale
- canNavigateTo — RBAC use case documentation
- navigate — Navigation method documentation
- subscribe — Subscription method documentation