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

14 KiB

Engineering Challenges, Vue Router Limitations, and Trade-offs

The fundamental mismatch

Vue Router is designed around an authoritative current route. A navigation resolves a location, runs guards, updates history, and makes that location current; <RouterView> then renders the matching component. That model is correct for normal web navigation.

Interactive native navigation needs a second, provisional route before any of those semantic effects are committed. The destination must be fully rendered beside the source while the user is still free to cancel. Much of the work in Native Vue Router exists to bridge that mismatch without forking Vue Router or relying on its internals.

Challenge 1: showing two route locations at once

Limitation

The usual <RouterView> follows the global current route. A CSS transition around a normal router view can animate old and new DOM after navigation, but it cannot naturally provide a live, reversible destination before navigation.

Approach

The runtime resolves and loads the destination, creates a preview entry, and renders explicit router views for both entries using the route prop. A scoped routeLocationKey is provided inside each entry so descendants using useRoute() read the correct route for that surface.

Trade-off

Two component trees may be live simultaneously. Preview components can mount before commit, so their setup and data loading must tolerate cancellation. Irreversible side effects should be tied to committed application state or view activation, not blindly to component mount.

Challenge 2: preserving Vue Router authority

Limitation

It would be simpler to maintain a completely separate navigation stack and update the URL afterward, but that would bypass guards, redirects, route encoding, and existing Vue Router integrations.

Approach

The preview phase never mutates Vue Router history. Commit uses public Vue Router navigation operations. The runtime then accepts the actual resulting route, including redirects, as authoritative and discards any stale preview.

Trade-off

The visual runtime must maintain and reconcile a second ledger. This is deliberate duplication of visual/navigation bookkeeping, with explicit invariants to keep the two systems aligned.

Challenge 3: predictive back and opaque browser history

Limitation

The browser does not expose a portable array of prior route locations. Calling router.back() also does not synchronously reveal the destination. A cold-start deep link may have an external page, another application, or no useful same-app route behind it.

Approach

The runtime records committed native view keys and uses that ledger for warm-session predictive back. Routes can declare a logical parent for cold-start prediction. The actual pop still goes through Vue Router/browser history when a real entry exists.

Trade-off

Applications must describe route topology where history alone is insufficient. A declared parent is a product-level relationship, not proof that the corresponding browser history entry exists. Synthetic parents use replace semantics when committed.

Challenge 4: guards, redirects, and asynchronous loading

Limitation

Lazy modules, route guards, redirects, and browser pops resolve asynchronously. Meanwhile, gesture input and animation frames continue. A response from an old navigation can arrive after a newer interaction has started.

Approach

Every transaction and begin attempt receives a monotonically increasing identity. Async continuations verify that they still own the current attempt and transaction before mutating state. Guard rejection springs back; redirect results become authoritative; stale previews are removed.

Trade-off

Only one visual transaction is authoritative at a time. The runtime supports rapid sequential interruption, not multiple independent transitions mutating one navigator concurrently.

Challenge 5: interruption without a navigation cooldown

Limitation

The easiest animation model locks input until a transition completes. That creates a visible cooldown and feels unlike a native application. Simply cancelling animation promises is unsafe because Vue Router may already be committing history.

Approach

Settling motion and semantic navigation are treated separately. A new interaction can interrupt the spring immediately, but it waits for any already-started Vue Router navigation to resolve. The prior transaction is finalized at the route Vue Router accepted, and the new transaction begins from that authoritative state.

Pointer recognizers also detach their local state before awaiting anything. Captured progress, velocity, and transaction ID travel with the old release callback, while a new pointer can start cleanly. Both move and release continuations check ownership before cleanup.

Trade-off

A route guard or browser-history operation can still impose real latency because semantic navigation cannot safely be cancelled after the platform has begun it. The library removes animation cooldown; it cannot remove application guard or network latency.

Challenge 6: direction and topology for sibling routes

Limitation

Vue Router knows route hierarchy and matching, but not that /inbox, /stories, and /profile are ordered pages on a horizontal strip. Browser history order also does not necessarily match visual tab order.

Approach

The navigator receives its peer route list, while routes declare siblingOrder and optional sibling history semantics. Direction is derived from route order. The sibling presentation moves both surfaces one-to-one, rather than placing a new foreground layer over the old one.

Trade-off

Visual topology must be explicit. Automatic inference from route declaration order would be fragile in modular or dynamically registered route sets.

Challenge 7: velocity that behaves consistently

Limitation

Raw pointer velocity in pixels per millisecond changes meaning with viewport size. A fixed-duration animation also ignores whether the user released slowly or flicked decisively.

Approach

Velocity is normalized by the gesture surface width or height and expressed as route progress per second. It influences both the commit decision and the rate of the damped settling spring. High-speed springs are advanced in small simulation substeps to avoid numerical instability.

Trade-off

The current spring constants and velocity scaling are shared defaults rather than a fully configurable physics system. Extremely high input is clamped, preserving stability over perfectly reproducing every raw pointer sample.

Challenge 8: gesture arbitration

Limitation

A horizontal movement may mean browser back, application back, tab paging, a component action, text selection, or ordinary scrolling. Bubbling alone is insufficient because a partially revealed child layer can steal the physical edge from its navigator.

Approach

Leading-edge back recognition runs in the navigator's capture phase. Component gestures own non-edge drags by stopping propagation. Recognition waits for a directional threshold, then uses pointer capture. Form controls and explicit ignore regions are excluded, while CSS touch-action leaves the perpendicular scroll axis available.

Trade-off

Applications must design gesture regions intentionally. Highly interactive canvases, maps, carousels, editors, and nested horizontal scrollers should opt out or provide their own arbitration.

Challenge 9: native edge gestures on the web

Limitation

Web content cannot set WKWebView.allowsBackForwardNavigationGestures. In a normal iOS Safari tab, the browser may reserve an edge sequence before page JavaScript receives enough input to implement its own predictive back.

Approach

The installed iOS PWA uses a non-passive leading-edge touch guard at capture time and disables horizontal overscroll as far as web content permits. The guard is intentionally inactive in a normal browser tab. Electron disables Chromium overscroll history navigation at the host level. Capacitor supplies the deterministic native container option.

Trade-off

An installed PWA can provide a strong approximation, not an absolute WebKit-level guarantee. Products that require complete ownership of the native back gesture should use the Capacitor host.

Challenge 10: view caching without corrupting history

Limitation

Keeping a tab mounted is a rendering concern; deciding whether Back should visit it is a history concern. Treating one list as both caused stale conversations and replaced tabs to appear as incorrect back targets.

Approach

The runtime separates the history-key ledger from the mounted-view cache. A replaced sibling is created lazily and can remain reusable without entering the back path. Popped pushed routes and guard-rejected destinations are evicted; history descriptors remain available for reconstruction. Statuses distinguish active, inactive, preview, and evicted entries. Route-aware active/visible effects give cached components a way to suspend work.

Trade-off

Mounted routes consume memory. The inactive cache is bounded and can be trimmed by the host, while cache: false and cache: 'pin' make exceptional route policy explicit. Evicted component-local state is not guaranteed to survive. Durable state belongs in Pinia, another store, IndexedDB, or the backend.

Vue's <KeepAlive> was not used as the cache owner. One shared wrapper is designed to select a current child, but a predictive gesture renders two route instances concurrently. Creating independent wrappers per temporary route layer would make wrapper lifetime control cache lifetime and complicate deterministic LRU eviction. The trade-off is a small router-owned cache with lifecycle APIs instead of Vue's built-in activated/deactivated hooks.

Challenge 11: accessibility with concurrent routes

Limitation

Two visible DOM subtrees can create duplicate landmarks, focus targets, and screen-reader content. A hidden cached route can also accidentally receive pointer or keyboard input.

Approach

Inactive entries are hidden, inert, and aria-hidden. Only the active route or interactive pair is presented visually. Back and tab controls remain semantic buttons and links. Reduced-motion preference settles transactions immediately.

Trade-off

Custom presentations and application overlays must preserve these invariants. Focus transfer at commit may still require application-specific handling for complex screens.

Challenge 12: reliable PWA updates

Limitation

Service-worker lifecycle, HTTP caching, Safari foreground behavior, and installed Home Screen state can leave an old application shell active even after a deployment. Reloading immediately is also dangerous during an interactive transaction.

Approach

Production builds use automatic worker activation and check on registration, focus, foreground resume, reconnection, and a periodic timer. Reload is deferred until the native transaction is idle. The demo exposes a build ID and update-check count, and local development/preview uses conservative cache headers.

Trade-off

Deployment infrastructure must still avoid long-lived caching for sw.js and the HTML shell. A client running code from before the automatic update policy may need one final manual refresh or reinstall; new code cannot retroactively change an old worker's behavior before it is loaded.

Current limitations

  • Client-side DOM navigation is the first-class target; SSR and hydration of a live visual stack are not currently a complete feature.
  • One runtime coordinates one authoritative visual transaction at a time. Independent nested navigation controllers need an explicit ownership design.
  • navigator and siblingGroup metadata are reserved topology fields; the current NativeNavigator still receives its sibling route list explicitly. Likewise, gesture: false is the enforced opt-out, while finer edge/full policy is primarily expressed by navigator and component structure today.
  • Route previews may mount application code that later gets cancelled.
  • Cold-start predictive back requires declared parent topology.
  • Cache eviction does not preserve arbitrary component-local state.
  • The browser cannot guarantee native gesture suppression at the same level as Capacitor or a custom WKWebView.
  • Presentation physics are currently library defaults rather than route-by-route configurable tokens.
  • Long sessions keep lightweight route descriptors in the ledger even when their component trees are evicted. Mounted DOM is bounded, but applications with exceptionally large histories may eventually benefit from indexed lookup and descriptor compaction.
  • Native appearance is broader than navigation: slow screen rendering, non-native controls, layout shifts, or inappropriate typography can still break the illusion.

Summary of deliberate trade-offs

Decision Benefit Cost
Keep Vue Router authoritative Guards, URLs, redirects, and ecosystem compatibility Reconciliation complexity
Render a preview before commit Truly interactive and cancellable navigation Two live component trees and preview side effects
Maintain separate history and view ledgers Correct back semantics plus tab caching More state and invariants
Interrupt springs but finish semantic commits No animation cooldown without corrupting history Guard/history latency can remain
Require explicit route topology Deterministic parent and sibling behavior More route metadata
Bound mounted views Predictable DOM and memory use Component-local state can be evicted
Use progressive platform adapters One core across PWA, Electron, and Capacitor Browser/PWA guarantees remain weaker than native hosts