subscribe - greydragon888/real-router GitHub Wiki
-
What it does: Simplified API for subscribing to successful navigation transitions. Calls callback on each
TRANSITION_SUCCESSevent, passing an object with current and previous router states. -
When to use:
- For reacting to route changes in the application
- For integration with UI frameworks (React, Vue, Angular)
- For logging and navigation analytics
- When you need simple subscription without working with specific event types
subscribe(listener: SubscribeFn): Unsubscribetype SubscribeFn = (state: {
route: State;
previousRoute: State | undefined;
}) => void;
type Unsubscribe = () => void;// Basic subscription
const unsubscribe = router.subscribe(({ route, previousRoute }) => {
console.log(`Navigated from ${previousRoute?.name} to ${route.name}`);
});
// Analytics integration
router.subscribe(({ route }) => {
analytics.trackPageView(route.path);
});
// Conditional logic based on transition
router.subscribe(({ route, previousRoute }) => {
if (route.name === "checkout" && previousRoute?.name !== "cart") {
showWarningModal();
}
});
// Unsubscribe on component unmount
useEffect(() => {
const unsubscribe = router.subscribe(({ route }) => {
setCurrentRoute(route);
});
return () => unsubscribe();
}, []);-
Type:
SubscribeFn— function accepting{ route, previousRoute }object - Purpose: Callback invoked on each successful transition
-
Error behavior:
TypeErrorif not a function
| Field | Type | Description |
|---|---|---|
route |
State |
Target state (new route) |
previousRoute |
State | undefined |
Previous state (may be undefined on first transition) |
-
Type:
Unsubscribe(() => void) - Purpose: Function to unsubscribe from events
-
Behavior:
- Call cancels subscription
- Safe for multiple calls
- After unsubscribe, callback won't receive new events
- Listener registration: Subscribes to successful transitions
-
Callback format: The listener receives a
{ route, previousRoute }object on each successful transition
| Condition | Error Type | Message |
|---|---|---|
| Listener is not a function | TypeError |
[router.subscribe] Expected a function... |
| Listener is null | TypeError |
[router.subscribe] Expected a function... |
| Listener is an object | TypeError |
[router.subscribe] Expected a function... |
Note: For subscription with an observer object, use @real-router/rx: observable(router).subscribe(observer).
| Method | When to use |
|---|---|
addEventListener() |
For subscribing to specific event types |
observable(router) from @real-router/rx
|
For Observable API (RxJS-compatible) |
navigate() |
Initiates transition that will trigger subscribe
|
- Successful subscription: Returns unsubscribe function
-
Receiving events: Callback is called on each
TRANSITION_SUCCESS -
Data format: Callback receives
{ route: State, previousRoute: State | undefined } -
Unsubscribe: After calling
unsubscribe(), callback is not invoked
// Subscription returns a function
const unsubscribe = router.subscribe(() => undefined);
expect(typeof unsubscribe).toStrictEqual("function");
// Callback receives correct data
const listener = vi.fn();
router.subscribe(listener);
const startState = await router.start("/home");
// First navigation - previousRoute is undefined
expect(listener).toHaveBeenCalledWith({
route: startState,
previousRoute: undefined,
});
// Subsequent navigation - previousRoute is the previous state
const toState = await router.navigate("users");
expect(listener).toHaveBeenCalledWith({
route: toState,
previousRoute: startState,
});const spy1 = vi.fn();
const spy2 = vi.fn();
router.subscribe(spy1);
router.subscribe(spy2);
await router.start("/home");
await router.navigate("users");
expect(spy1).toHaveBeenCalled();
expect(spy2).toHaveBeenCalled();const spy = vi.fn();
const unsubscribe = router.subscribe(spy);
unsubscribe();
await router.start("/home");
await router.navigate("users");
expect(spy).not.toHaveBeenCalled(); // Unsubscribed, won't receive eventsconst spy = vi.fn();
const unsubscribe = router.subscribe(spy);
unsubscribe();
expect(() => {
unsubscribe(); // Safe
}).not.toThrowError();-
First navigation:
previousRoutewill beundefined -
TypeError for non-functions: Throws error for
null, objects, strings, etc. -
Object with subscribe method: NOT supported (use
@real-router/rx)
// ❌ Does NOT work — subscribe accepts only functions
router.subscribe({ next: (state) => console.log(state) });
// TypeError: Expected a function
// ✅ For observer object use @real-router/rx
import { observable } from "@real-router/rx";
observable(router).subscribe({
next: (state) => console.log(state),
});- Callback is called synchronously on
TRANSITION_SUCCESSevent - Data is passed in
{ route, previousRoute }format -
routeis always aStateobject -
previousRoutemay beundefinedon first transition - Unsubscribe function is safe for multiple calls
- Listener validation happens when
subscribe()is called
subscribe() and subscribeLeave() are symmetric — one for arrivals, one for departures:
subscribe() |
subscribeLeave() |
|
|---|---|---|
| Event | TRANSITION_SUCCESS | TRANSITION_LEAVE_APPROVE |
| Timing | After state changes | Before state changes |
router.getState() |
New route | Still current route |
| Async support | No (fire-and-forget) | Yes — Promise<void> blocks pipeline |
| AbortSignal | No | Yes — signal in callback state |
| Use for | Arrival side-effects | Departure side-effects, exit animations |
// subscribe: "I arrived here"
router.subscribe(({ route, previousRoute }) => {
analytics.trackPageView(route.name);
});
// subscribeLeave: "I'm leaving here"
router.subscribeLeave(({ route, nextRoute }) => {
sessionStorage.setItem("scroll:" + route.name, String(window.scrollY));
});subscribeLeave() only fires when departure is confirmed — after all canDeactivate guards pass. If a guard blocks the navigation, the callback never runs.
See leave.md for subscribeLeave() full documentation.
| Master | Current | |
|---|---|---|
| Signature | (listener) => unsubscribe | { unsubscribe } |
(listener: SubscribeFn) => Unsubscribe |
| Object support | Yes ({ next: fn }) |
No (functions only) |
| Return value | Function or { unsubscribe }
|
Always function |
| Validation | None |
TypeError for non-functions |
| Typing | JavaScript (no types) | Full TypeScript typing |
Was (master):
// Function — returns unsubscribe function
const unsubscribe = router.subscribe((state) => console.log(state));
unsubscribe();
// Object with next — returns { unsubscribe }
const subscription = router.subscribe({
next: (state) => console.log(state),
});
subscription.unsubscribe();Now (current):
// Function — works as before
const unsubscribe = router.subscribe((state) => console.log(state));
unsubscribe();
// Object — TypeError
router.subscribe({ next: (state) => console.log(state) });
// TypeError: [router.subscribe] Expected a function.
// For Observable pattern use router[Symbol.observable]().subscribe(observer)Impact: Code using router.subscribe({ next: fn }) will stop working. Migration to @real-router/rx Observable API required.
Was (master):
// For function
const result1 = router.subscribe(fn);
typeof result1; // "function"
// For object
const result2 = router.subscribe({ next: fn });
typeof result2; // "object" { unsubscribe: fn }Now (current):
// Always function
const result = router.subscribe(fn);
typeof result; // "function"Impact: Code expecting an object with unsubscribe method will receive a function. In practice, most code only called the result.
Was (master):
router.subscribe(null);
// Silently created subscription, crashed on event:
// TypeError: Cannot read properties of null (reading 'call')Now (current):
router.subscribe(null);
// TypeError: [router.subscribe] Expected a function.Impact: Improvement. Errors are detected immediately on call, not later on event.
-
Observer object support: Moved to
@real-router/rxpackage -
Auto-detect listener type: Removed
typeof listener === 'object' - Polymorphic return: Always returns function
-
Strict typing:
SubscribeFnandUnsubscribetypes -
Listener validation:
TypeErrorfor non-functions -
Informative error message: Points to alternative (
@real-router/rx)
-
Separation of concerns:
subscribe()— simple API for functions,@real-router/rx— full Observable API - Code simplification: Removed conditional logic for objects
- Find all calls to
router.subscribe({ next: ... }) - Migrate to
observable(router).subscribe({ next: ... })from@real-router/rx - Update return value handling (if used
.unsubscribe()) - Verify that listener is always a function
Observer object → @real-router/rx
// ❌ Old code
const subscription = router.subscribe({
next: (state) => console.log(state.route.name),
});
subscription.unsubscribe();
// ✅ New code
import { observable } from "@real-router/rx";
const subscription = observable(router).subscribe({
next: (state) => console.log(state.route.name),
});
subscription.unsubscribe();Function — no changes
// ✅ Works as before
const unsubscribe = router.subscribe(({ route }) => {
console.log(route.name);
});
unsubscribe();Return type checking
// ❌ Old code — type checking
const result = router.subscribe(listener);
if (typeof result === "function") {
result();
} else {
result.unsubscribe();
}
// ✅ New code — always function
const unsubscribe = router.subscribe(listener);
unsubscribe();| Category | Status |
|---|---|
| Breaking Changes | HIGH (removed object support) |
| New validation | ✅ TypeError for non-functions |
| API simplification | ✅ Unified return type |
| Typing | ✅ Full TypeScript typing |
Maximum severity: HIGH
Code using
router.subscribe({ next: fn })requires migration toobservable(router).subscribe()from@real-router/rx. The new API is cleaner and follows the separation of concerns principle:subscribe()for simple callbacks,@real-router/rxfor the full Observable pattern.