subscribe - greydragon888/real-router GitHub Wiki

subscribe

1. Overview

  • What it does: Simplified API for subscribing to successful navigation transitions. Calls callback on each TRANSITION_SUCCESS event, 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

2. Signature

subscribe(listener: SubscribeFn): Unsubscribe

Types

type SubscribeFn = (state: {
  route: State;
  previousRoute: State | undefined;
}) => void;
type Unsubscribe = () => void;

Usage Examples

// 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();
}, []);

3. Parameters

listener (required)

  • Type: SubscribeFn — function accepting { route, previousRoute } object
  • Purpose: Callback invoked on each successful transition
  • Error behavior: TypeError if not a function

Callback Argument Structure

Field Type Description
route State Target state (new route)
previousRoute State | undefined Previous state (may be undefined on first transition)

4. Return Value

  • 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

5. Side Effects

  • Listener registration: Subscribes to successful transitions
  • Callback format: The listener receives a { route, previousRoute } object on each successful transition

6. Possible Errors

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).

7. Related Methods

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

8. Behavior

Main Scenarios

  • 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

Test Examples

// 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,
});

Multiple Subscribers

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();

Unsubscribe

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 events

Multiple Unsubscribe Calls

const spy = vi.fn();
const unsubscribe = router.subscribe(spy);

unsubscribe();

expect(() => {
  unsubscribe(); // Safe
}).not.toThrowError();

Edge Cases

  • First navigation: previousRoute will be undefined
  • TypeError for non-functions: Throws error for null, objects, strings, etc.
  • Object with subscribe method: NOT supported (use @real-router/rx)

Difference from Observable API

// ❌ 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),
});

Guarantees

  • Callback is called synchronously on TRANSITION_SUCCESS event
  • Data is passed in { route, previousRoute } format
  • route is always a State object
  • previousRoute may be undefined on first transition
  • Unsubscribe function is safe for multiple calls
  • Listener validation happens when subscribe() is called

Symmetric Pair: subscribeLeave()

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.


Migration from router5

Version Comparison

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

1. Breaking Changes

HIGH — Removed observer object support

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.

MEDIUM — Unified return type

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.

LOW — Added parameter validation

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.

2. Implementation Changes

Removed Functionality

  • Observer object support: Moved to @real-router/rx package
  • Auto-detect listener type: Removed typeof listener === 'object'
  • Polymorphic return: Always returns function

New Functionality

  • Strict typing: SubscribeFn and Unsubscribe types
  • Listener validation: TypeError for non-functions
  • Informative error message: Points to alternative (@real-router/rx)

Architectural Changes

  • Separation of concerns: subscribe() — simple API for functions, @real-router/rx — full Observable API
  • Code simplification: Removed conditional logic for objects

3. Migration Guide

Checklist

  • 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

Migration Examples

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();

4. Summary

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 to observable(router).subscribe() from @real-router/rx. The new API is cleaner and follows the separation of concerns principle: subscribe() for simple callbacks, @real-router/rx for the full Observable pattern.

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