lifecycle plugin - greydragon888/real-router GitHub Wiki
@real-router/lifecycle-plugin
1. Overview
- Name: Lifecycle Plugin
- Package:
@real-router/lifecycle-plugin - Purpose: Route-level lifecycle hooks —
onEnter,onStay,onLeave— declarative side-effects attached directly to route definitions. - Typical scenarios:
- Analytics tracking when entering a route
- Cleaning up timers, WebSockets, or draft state when leaving a route
- Refreshing data when route params change (same route, different params)
- Prefetching data on route entry
2. Installation and Setup
npm install @real-router/lifecycle-plugin
# or
pnpm add @real-router/lifecycle-plugin
import { createRouter } from "@real-router/core";
import { lifecyclePluginFactory } from "@real-router/lifecycle-plugin";
const routes = [
{
name: "dashboard",
path: "/dashboard",
onEnter: (toState) => analytics.track("dashboard_viewed"),
onLeave: () => autosaveTimer.clear(),
},
{
name: "users.view",
path: "/users/:id",
onStay: (toState, fromState) => {
console.log("User changed:", fromState.params.id, "→", toState.params.id);
},
},
];
const router = createRouter(routes);
router.usePlugin(lifecyclePluginFactory());
await router.start("/dashboard");
3. Configuration
The plugin takes no configuration. The hooks themselves — defined on route objects — are the configuration.
Types
type LifecycleHook = (toState: State, fromState: State | undefined) => void;
All hooks receive the same signature. Return values are ignored.
Module Augmentation
Importing the plugin package extends the Route interface with typed fields:
// Automatically available after importing the plugin
interface Route {
onEnter?: LifecycleHook;
onStay?: LifecycleHook;
onLeave?: LifecycleHook;
}
4. Lifecycle Hooks
| Plugin Hook | Implemented | Description |
|---|---|---|
onStart |
❌ | |
onStop |
❌ | |
onTransitionStart |
❌ | |
onTransitionLeaveApprove |
✅ | Calls onLeave on the leaving route |
onTransitionSuccess |
✅ | Calls onEnter or onStay on the target route |
onTransitionError |
❌ | |
onTransitionCancel |
❌ | |
teardown |
❌ | Stateless — no cleanup needed |
Route-Level Hook Semantics
| Hook | Fires when | Router event | Route checked |
|---|---|---|---|
onEnter |
Route is entered | TRANSITION_SUCCESS |
toState.name |
onStay |
Same route, params changed | TRANSITION_SUCCESS |
toState.name |
onLeave |
Route is left | TRANSITION_LEAVE_APPROVE |
fromState.name |
Execution Order
navigate("newRoute")
│
├── deactivation guards pass
│
├── TRANSITION_LEAVE_APPROVE
│ └── onLeave(toState, fromState) on fromState.name
│
├── activation guards pass
│
└── TRANSITION_SUCCESS
└── onEnter(toState, fromState) on toState.name
For same-route navigation (params change):
navigate("sameRoute", { id: "2" })
│
└── TRANSITION_SUCCESS
└── onStay(toState, fromState) on toState.name
Note: onLeave does not fire for same-route navigation.
5. Router Interaction
Router Interface Extension
The plugin does not extend the Router interface. It's a purely observational plugin.
Hook Resolution
Hooks are read from route custom fields via api.getRouteConfig(routeName). Custom fields are any route properties beyond the standard set (name, path, children, canActivate, canDeactivate, forwardTo, encodeParams, decodeParams, defaultParams).
Data Flow
Route definition: { name: "home", path: "/", onEnter: fn }
│
├── Core extracts custom fields → routeCustomFields["home"] = { onEnter: fn }
│
└── Plugin reads via api.getRouteConfig("home")?.onEnter
└── typeof === "function" → call fn(toState, fromState)
6. Behavior
Leaf Route Only
Hooks fire only for the leaf route (toState.name / fromState.name), not for parent segments. Navigation from home to users.view calls onLeave on home and onEnter on users.view — not on users.
Error Handling
Errors from hooks propagate to the EventEmitter, which logs them to stderr. The router continues operating — transitions are never blocked by hook errors.
Edge Cases
| Scenario | Behavior |
|---|---|
| Route without hooks | No-op, no errors |
| Non-function value in hook field | Silently skipped (typeof check) |
Initial router.start() |
onEnter fires with fromState = undefined |
onLeave on initial start |
Does not fire (no previous route) |
| Hook throws an error | Error logged to stderr, transition continues |
onLeave + transition fails at activation |
onLeave already fired (leave-approve phase) |
7. Integration with Other Plugins
| Plugin | Interaction |
|---|---|
@real-router/browser-plugin |
Works correctly, hooks fire on popstate too |
@real-router/logger-plugin |
Logger logs transition, lifecycle hooks also fire |
@real-router/persistent-params-plugin |
Persistent params visible in hook's toState |
Registration Order
No ordering constraints. The plugin observes events — it doesn't modify state.
router.usePlugin(lifecyclePluginFactory()); // Any position
router.usePlugin(browserPluginFactory());
router.usePlugin(loggerPluginFactory());
8. Usage Examples
Analytics Tracking
const routes = [
{
name: "product",
path: "/products/:id",
onEnter: (toState) => {
analytics.track("product_viewed", { productId: toState.params.id });
},
},
];
Cleanup on Leave
const routes = [
{
name: "editor",
path: "/editor/:docId",
onLeave: () => {
autosaveTimer.clear();
webSocket.disconnect();
},
},
];
React to Param Changes
const routes = [
{
name: "users.view",
path: "/users/:id",
onStay: (toState) => {
userStore.loadUser(toState.params.id);
},
},
];
Combined Hooks
const routes = [
{
name: "chat",
path: "/chat/:roomId",
onEnter: (toState) => {
chatSocket.connect(toState.params.roomId);
},
onStay: (toState, fromState) => {
chatSocket.switchRoom(fromState.params.roomId, toState.params.roomId);
},
onLeave: () => {
chatSocket.disconnect();
},
},
];
See Also
- Plugin Architecture — How plugins integrate with the router
- subscribe — Global transition subscription (alternative approach)
- leave — Global leave subscription
- Route — Route configuration reference
- Navigation Lifecycle — Full transition event sequence
- @real-router/logger-plugin — Development logging plugin
- @real-router/browser-plugin — Browser History API plugin