Files
Native-Router-Vue/docs/principles-and-scalability.md
2026-07-22 02:12:05 +00:00

12 KiB

Core Principles, Scalability, and Flexibility

Core principles

1. Vue Router remains authoritative

Native Vue Router is a visual transaction system around Vue Router, not a competing URL router. Matching, encoding, guards, redirects, and committed history stay with Vue Router. If the two systems disagree after navigation, Vue Router's accepted route wins.

This principle preserves compatibility and gives the runtime a clear recovery rule.

2. Preview is not commitment

A destination may be loaded, mounted, and visible without being current. Gesture progress must never imply semantic commitment. The URL, browser history, analytics, and irreversible business actions should change only when the transaction commits.

3. Gestures are first-class navigation input

A gesture is not a decorative transition attached after router.push(). It begins a navigation candidate, controls its progress, and chooses commit or cancellation. Buttons, links, hardware back, and gestures feed the same runtime so they cannot develop contradictory behavior.

4. Navigation must remain interruptible

Users should not wait for visual settling before expressing the next intent. Animations are disposable; committed route decisions are not. New input may interrupt presentation immediately while respecting any semantic operation already in flight.

5. Route topology should be explicit

History order, visual sibling order, and logical parentage are different concepts. Applications declare parent and sibling relationships rather than relying on incidental route registration or click history.

6. History and rendering are separate concerns

A route can be cached without belonging in the back stack, and a back destination can be reconstructed without remaining mounted. This separation is essential for tabs, sheets, deep links, and bounded memory.

7. Async work never owns state forever

Every preload, pointer continuation, route commit, and animation frame is conditional on a current attempt or transaction identity. Stale work becomes a no-op. Cleanup is ownership-aware and cannot erase a newer gesture.

8. The active frame should be cheap

Pointer movement updates normalized progress. Rendering derives from that value. Built-in sibling motion uses only adjacent transforms, and JavaScript work per frame does not grow with route count. Route components should avoid layout churn and heavyweight synchronous work during previews.

9. Host capabilities are adapters, not conditionals scattered through core

Hardware back, haptics, deep links, WebKit edge behavior, and Electron command-line switches belong at the platform boundary. The transaction model remains the same across hosts.

10. Accessibility is a state invariant

Cached and preview DOM must not create duplicate interactive applications. Inactive layers are isolated from focus and assistive technology, controls keep native semantics, and reduced-motion behavior is deterministic.

Scalability model

The runtime is intended to scale in four different ways: number of routes, session length, application complexity, and number of host platforms.

Route and DOM scale

Only the active route, previously visited inactive routes, and a transaction preview need mounted component trees. Siblings are not instantiated eagerly at application startup. maxInactive bounds the inactive mounted cache; the default is four. Pinned entries are deliberately outside this ordinary budget. Older views are marked evicted and lazily remounted when needed.

During an interaction, animation work concerns two surfaces regardless of total route count:

Resource Growth behavior
Animated surfaces Constant: from and to
Mounted inactive component trees Bounded by maxInactive
Route descriptors/history keys Grows with navigation history
Per-frame transaction state Constant
Lazy route code Loaded on first preview or navigation

The current implementation uses linear searches through view entries for some reconciliation operations. This is appropriate for ordinary application histories and a small mounted cache. If the runtime is used for sessions with thousands of unique committed entries, a key/path index and descriptor compaction would be a sensible evolution without changing the public transaction model.

Data scale

The view cache is not an application data cache. Large collections, message history, drafts, and durable form state should live outside route component instances. This allows view eviction to remain cheap and makes state available whether a route is reached through a gesture, deep link, background notification, or restored session.

Cached components should also avoid doing active-screen work indefinitely. Route-aware lifecycle effects let polling, animation loops, media, and subscriptions stop while a component is inactive and resume without losing its local render state. Hosts can call trimCache() under memory pressure; the Capacitor adapter does so when the app pauses by default.

Preview loading should fetch only what the destination needs to render its initial surface. Applications can use route-level lazy imports, shared stores, request deduplication, and cancellation to avoid duplicating expensive work during a cancelled preview.

Team and feature scale

Navigation policy is carried by route metadata and small primitives rather than screen-specific animation code. Feature teams can define:

  • Presentation and gesture policy on their routes.
  • Logical parents for deep-linked screens.
  • Sibling order and history behavior for peer lists owned by a navigator.
  • Component-owned gesture entry points.

Cross-cutting behavior remains in the runtime. This reduces the risk that every feature implements a slightly different back threshold, history mutation, or animation lock.

For very large applications, route topology should be assembled from typed feature modules and validated in tests. Parent cycles, duplicate sibling ordering, and incompatible nested gesture regions are application configuration errors and should be caught before runtime.

Platform scale

The NativePlatformAdapter interface keeps platform growth additive. A new host can install listeners, provide haptics, or coordinate root exit without changing route matching or presentation code. The existing PWA, Electron, and Capacitor adapters demonstrate three capability levels:

  1. Best-effort control from web content.
  2. Desktop host control around a web renderer.
  3. Native mobile container integration.

Platform-specific policy should not leak into route components unless the product experience genuinely differs.

Flexibility and extension points

Route metadata

Metadata expresses topology and defaults close to route definitions:

{
  path: '/chat/:id',
  component: () => import('./ChatView.vue'),
  meta: {
    native: {
      parent: '/inbox',
      presentation: 'push',
      gesture: 'edge',
    },
  },
}

Ordered peers are passed to NativeNavigator. Their routes use siblingOrder to derive direction, while siblingHistory decides whether selection replaces or grows history. Metadata supplies defaults, while individual runtime calls can override presentation, direction, replacement, and source geometry. gesture: false disables navigator gesture handling for a route; finer ownership remains structural in the current implementation.

Custom presentations

Presentation definitions receive only the data needed to derive layer styles:

nativeRouter.registerPresentation(
  definePresentation({
    name: "scale-fade",
    axis: "x",
    layerStyle({ role, progress }) {
      return role === "to"
        ? { opacity: progress, transform: `scale(${0.94 + progress * 0.06})` }
        : { opacity: 1 - progress * 0.25 };
    },
  }),
);

A presentation does not decide history or commit. Keeping motion separate from navigation semantics makes new visual styles safer to add.

Custom recognizers

Applications with a bespoke interaction can call:

  • beginInteractive() to create and preload a candidate.
  • updateInteractive() with normalized progress and velocity.
  • finishInteractive() to apply the normal commit decision.
  • cancelInteractive() to settle back.

This permits interactions such as a card expansion, trackpad scrub, keyboard-driven preview, or canvas gesture without duplicating the route transaction machinery.

Custom recognizers must follow the same ownership rules: one current transaction, normalized input, stale-callback protection, and explicit cancellation on teardown.

Visual presets

The core owns behavior and minimum presentation CSS. Higher-level packages can supply tab bars, back controls, motion tokens, typography, and platform-adaptive appearance. Product teams can replace the preset without replacing the transaction runtime.

Platform adapters

Adapters can install host listeners and optionally provide haptic feedback or root-exit behavior. They should translate host events into runtime operations instead of editing runtime ledgers directly.

Adoption patterns

The architecture supports incremental use:

  1. Imperative transitions only: use push, pop, sibling, present, and dismiss with buttons and links.
  2. Predictive back: wrap the route surface in NativeNavigator and declare cold-start parents.
  3. Horizontal route paging: add ordered sibling routes.
  4. Component-originated navigation: wrap selected rows or cards with NativeGestureLink.
  5. Custom product motion: register presentations or drive transactions from a custom recognizer.
  6. Host integration: add the PWA, Electron, or Capacitor adapter according to the guarantees required.

Teams do not need to make every route gesture-driven at once. Route metadata can disable gestures while retaining native runtime navigation.

Reliability and testing principles

Interactive routing failures are temporal, so tests must assert state during transitions rather than only final URLs. The test suite should preserve these invariants:

  • The from and to routes are both live during a held gesture.
  • The URL does not change before commit.
  • Sibling direction follows declared order.
  • Adjacent siblings remain edge-to-edge at intermediate progress.
  • Navigating to the active route is a strict no-op.
  • A guard rejection removes the preview and restores the source.
  • Back never selects an unrelated cached view.
  • A new click or gesture can interrupt settling.
  • Rapid pointer releases cannot leave an orphaned transaction.
  • Fast, short flicks commit through velocity and settle faster.
  • Modal and sheet presentation is independent of previous history shape.
  • PWA worker activation and update checks remain observable.

Final-state tests alone would miss most of the bugs that make a router feel non-native.

Evolution rules

Future work should preserve the following boundaries:

  • Do not make preview routes authoritative early to simplify animation.
  • Do not infer back targets from the mounted cache.
  • Do not make presentation definitions mutate history.
  • Do not solve platform behavior with host checks scattered through core.
  • Do not introduce input locks as a substitute for correct interruption.
  • Do not let stale async cleanup run without verifying ownership.
  • Do not rely on component mount as proof of committed navigation.

Likely extensions include configurable spring profiles, indexed ledgers for unusually long sessions, stronger focus restoration, explicit multi-navigator ownership, SSR-safe initial stack hydration, and more platform-specific motion presets. Each can be added while retaining the same central model: preview visually, commit semantically, and reconcile authoritatively.