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