STRATEGY: 'push' (default)
══════════════════════════════════════════
User Journey:
/home → /about → /contact
Browser History Stack:
┌─────────────┐
│ /contact │ ← Current (length: 3)
├─────────────┤
│ /about │ [Back button goes here]
├─────────────┤
│ /home │
└─────────────┘
Code:
router.load('contact', { historyStrategy: 'push' });
✓ Each navigation adds new entry
✓ Back button works as expected
✓ Forward button available after going back
✗ History grows unbounded
STRATEGY: 'replace'
══════════════════════════════════════════
User Journey:
/home → /about → /contact (replace)
Browser History Stack:
┌─────────────┐
│ /contact │ ← Current (length: 2)
├─────────────┤
│ /home │ [Back button goes here]
└─────────────┘
↑
/about was replaced by /contact
Code:
router.load('contact', { historyStrategy: 'replace' });
✓ No history pollution
✓ Good for redirects/corrections
✓ Prevents "back" to intermediate states
✗ Can't navigate back to replaced pages
STRATEGY: 'none'
══════════════════════════════════════════
User Journey:
/home → /about → /contact (none)
Browser History Stack:
┌─────────────┐
│ /home │ ← Current (length: 1)
└─────────────┘
URL bar shows: /contact
But history still has: /home
Code:
router.load('contact', { historyStrategy: 'none' });
✓ No history interaction at all
✓ Good for modal-style navigation
✗ Back button goes to previous app page, not /about
✗ URL and history out of sync
COMPARISON
══════════════════════════════════════════════════════════
Use Case | Strategy
─────────────────────────────────────────────────────────
Normal navigation | 'push'
Login redirect | 'replace'
Fixing invalid route | 'replace'
Multi-step form (same logical page)| 'replace'
Modal / overlay content | 'none'
Wizard steps (want back to work) | 'push'
Correcting user typos in URL | 'replace'
REAL-WORLD EXAMPLE: Login Flow
═══════════════════════════════════
// User tries to access protected route
canLoad() {
if (!isLoggedIn) {
// Redirect to login WITH replace
// So after login, "back" doesn't go to login page
router.load('login', { historyStrategy: 'replace' });
return false;
}
}
// After successful login
login() {
authenticate();
// Navigate to dashboard WITH replace
// So "back" from dashboard doesn't go to login
router.load('dashboard', { historyStrategy: 'replace' });
}
History progression:
1. User at /home
2. Tries /admin → redirected to /login (replace)
History: [/home, /login]
3. After login → /admin (replace)
History: [/home, /admin]
4. Back button → goes to /home (skips /login)
REAL-WORLD EXAMPLE: Wizard
═══════════════════════════════════
// Multi-step form
wizard.nextStep() {
currentStep++;
// Use push so back button works
router.load(`wizard/step${currentStep}`, {
historyStrategy: 'push'
});
}
History: /wizard/step1 → /wizard/step2 → /wizard/step3
Back button goes through steps correctly
REAL-WORLD EXAMPLE: Search Filters
══════════════════════════════════════
// User adjusts filters
applyFilters() {
// Use replace to update URL without history spam
router.load('search', {
queryParams: { ...filters },
historyStrategy: 'replace'
});
}
Without replace:
/search → /search?cat=A → /search?cat=A&sort=price
→ /search?cat=A&sort=price&page=2
→ /search?cat=A&sort=price&page=3
[User hits back 4 times to go back!]
With replace:
/search → /search?cat=A&sort=price&page=3
[User hits back once to go back!]
Scenario: Navigate from /users/1 to /users/2
(Same component, different parameter)
TRANSITION PLAN: 'replace' (default)
════════════════════════════════════════
/users/1 (ComponentA, id=1)
↓
router.load('/users/2')
↓
┌──────────────────────────────┐
│ 1. Unload current instance │
│ - unloading() called │
│ - detaching() called │
│ - Component destroyed │
└────────────┬─────────────────┘
↓
┌──────────────────────────────┐
│ 2. Create new instance │
│ - New component instance │
│ - canLoad() called │
│ - loading() called │
│ - attached() called │
│ - loaded() called │
└────────────┬─────────────────┘
↓
/users/2 (ComponentA, id=2) ← Different instance
Timeline:
ComponentA(id=1) ComponentA(id=2)
unloading()
detaching()
[destroyed]
canLoad()
loading()
attached()
loaded()
✓ Clean slate, no stale state
✓ Simple mental model
✗ Slower (full recreation)
✗ Loses component state
✗ Re-runs constructor, bound, etc.
TRANSITION PLAN: 'invoke-lifecycles'
════════════════════════════════════════
/users/1 (ComponentA, id=1)
↓
router.load('/users/2')
↓
┌──────────────────────────────┐
│ 1. Keep existing instance │
│ - Same component object │
│ - No destruction │
└────────────┬─────────────────┘
↓
┌──────────────────────────────┐
│ 2. Re-invoke hooks │
│ - canLoad() called │
│ - loading() called │
│ - loaded() called │
│ (NO attach/detach) │
└────────────┬─────────────────┘
↓
/users/2 (ComponentA, id=2) ← Same instance!
Timeline:
ComponentA(id=1)
canLoad(id=2)
loading(id=2)
loaded(id=2)
ComponentA(id=2)
✓ Faster (reuses instance)
✓ Can preserve component state
✓ Smoother transitions/animations
✗ Must handle state updates correctly
✗ Potential for stale data bugs
COMPARISON
══════════════════════════════════════════════════════════
Aspect | replace | invoke-lifecycles
────────────────────────────────────────────────────────────────
Instance | New | Reused
Speed | Slower | Faster
State | Fresh | Preserved*
Lifecycle hooks | All | Subset
DOM | Removed/readded | Stays
Use for | Default behavior | Param-only changes
* Preserved state can be a pro or con depending on use case
CONFIGURATION
═══════════════════════════════════════════════════════════
Global configuration:
@route({
transitionPlan: 'invoke-lifecycles', ← All routes
routes: [...]
})
Per-route configuration:
{
path: 'users/:id',
component: UserDetail,
transitionPlan: 'invoke-lifecycles' ← Just this route
}
Per-navigation override:
router.load('users/2', {
transitionPlan: 'invoke-lifecycles' ← Just this navigation
});
REAL-WORLD EXAMPLE: User Profile Tabs
═══════════════════════════════════════════════════════════
Component:
class UserProfile implements IRouteViewModel {
userId: string;
userData: User;
selectedTab = 'overview'; ← Component state
loading(params: Params) {
if (this.userId !== params.id) {
// Different user - fetch new data
this.userId = params.id;
this.userData = await fetchUser(params.id);
}
// Update tab from URL
this.selectedTab = params.tab || 'overview';
}
}
Routes:
{
path: 'users/:id/:tab?',
component: UserProfile,
transitionPlan: 'invoke-lifecycles' ← Preserve state
}
Navigation:
/users/123/overview → /users/123/posts
└─ Same user, keep loaded data, just update tab
/users/123/posts → /users/456/posts
└─ Different user, fetch new data in loading()
WHEN TO USE EACH
═══════════════════════════════════════════════════════════
Use 'replace' when:
✓ You want clean state each time
✓ Component has complex initialization
✓ Different params mean completely different data
✓ You don't trust yourself to handle reuse correctly
Use 'invoke-lifecycles' when:
✓ Only parameters change (same logical entity)
✓ You want to preserve UI state (scroll, selections)
✓ Performance matters (frequent navigation)
✓ You have good loading() logic that handles updates
COMMON PITFALL
═══════════════════════════════════════════════════════════
// ✗ BAD: Doesn't update when params change
class ProductDetail implements IRouteViewModel {
product: Product;
constructor() {
this.product = fetchProduct(params.id); ← params not available!
}
}
// ✓ GOOD: Updates on every navigation
class ProductDetail implements IRouteViewModel {
product: Product;
loading(params: Params) {
this.product = await fetchProduct(params.id); ← Correct!
}
}