Framework7 Router Architecture: Navigation Stacks, Trust Boundaries, and Operational Patterns
Framework7 Router maintains independent navigation stacks per view instance, enabling tabbed layouts without history collisions. Covers smallest design, trust boundaries, data serialization limits, operational monitoring, and failure modes like redirect loops.
28 Dec 2025, 21:16 UTC

The Problem: Independent History Trees in a Single Page
Framework7 applications often need multiple independent navigation contexts—tab bars, split views, or modal stacks—within one page. The core router solves this by maintaining a separate navigation stack per view instance. Each view holds its own router instance with isolated back/forward state, so navigating in one tab never pollutes another's history. This design avoids the single-history bottleneck of traditional SPAs but introduces boundaries you must manage explicitly: view initialization, guard execution order, and state persistence across reloads.
Smallest Suitable Design
For a single-page PWA, initialize one main view with a single router instance:
// app.js (Framework7 v8 + Vite)
import Framework7 from 'framework7/bundle';
import routes from './routes.js';
const app = new Framework7({
el: '#app',
routes,
view: {
iosDynamicNavbar: false,
mdDynamicNavbar: false
}
});
export default app;
Routes are plain objects with path, an async component (dynamic import()), and optional guards:
// routes.js
export default [
{
path: '/',
component: () => import('./pages/Home.vue')
},
{
path: '/profile/',
component: () => import('./pages/Profile.vue'),
beforeEnter: async (to, from, resolve) => {
const authed = await checkAuth();
authed ? resolve() : resolve({ path: '/login/' });
}
}
];
Run this in a Vite project scaffolded with npm init framework7@latest -- --framework=vue --bundler=vite. Requires Node 18+ and a browser supporting dynamic import (all modern targets). No special permissions beyond file-system access for the dev server.
Multi-View Initialization
Tabbed layouts need explicit view instances with unique IDs. In your main layout:
<!-- index.html or App.vue -->
<div id="app">
<div class="views tabs">
<div id="view-home" class="view view-main tab tab-active" data-router="true">
<div class="page"><div class="page-content">...</div></div>
</div>
<div id="view-settings" class="view tab" data-router="true">
<div class="page"><div class="page-content">...</div></div>
</div>
</div>
<div class="toolbar tabbar">
<a href="/" class="tab-link tab-link-active" data-tab="#view-home">Home</a>
<a href="/settings/" class="tab-link" data-tab="#view-settings">Settings</a>
</div>
</div>
Each view element with data-router="true" gets its own router instance. Verify in console: app.router.views.length === 2 and each view.router.history shows independent stacks.
Trust Boundaries: Guards Before Component Lifecycle
Route guards (beforeEnter, beforeLeave) execute in Framework7's router context before any framework component mounts. This is the correct place for authorization checks. If you defer to a Vue onMounted or React useEffect, the component renders briefly—a flash of unauthorized content—before redirecting.
Example guard with token validation:
// routes.js
{
path: '/admin/',
component: () => import('./pages/Admin.vue'),
beforeEnter: async (to, from, resolve) => {
try {
const token = localStorage.getItem('auth_token');
const valid = await validateToken(token); // your API call
if (!valid) throw new Error('expired');
resolve();
} catch {
// Single redirect—avoid loops by not re-checking on /login/
resolve({ path: '/login/', query: { redirect: to.fullPath } });
}
}
}
Limitation: guards run on every navigation, including browser back/forward. Cache validation results in memory (not localStorage) if the API call is expensive. A practical check: open DevTools Network tab, navigate to a guarded route, confirm only one guard request fires per transition.
Data Boundaries: Strings Only, No Auto-Persistence
Router params and query values are always strings. Complex objects must be serialized:
// Navigating with an object
router.navigate({
path: '/detail/',
query: { item: JSON.stringify({ id: 42, tags: ['a','b'] }) }
});
// In the target component
const item = JSON.parse($route.query.item || '{}');
Framework7 does not persist navigation state across page reloads. On refresh, the router re-matches the current URL from a cold start. If you need scroll position or form drafts preserved, integrate a state store (Pinia, Redux, Svelte store) and hydrate it in a top-level beforeEnter guard or app initialization hook.
Framework Integration Wrappers
Vue, React, and Svelte integrations delegate to Framework7's core router for native transitions while preserving framework reactivity. In Vue 3, use defineAsyncComponent with Framework7's loader wrapper—not native Suspense:
// Vue 3 route component
import { defineAsyncComponent } from 'vue';
import { f7route } from 'framework7-vue';
export default {
components: {
ProfilePage: defineAsyncComponent(() => import('./Profile.vue'))
},
setup() {
const route = f7route(); // reactive route proxy
return { route };
}
};
React and Svelte follow similar patterns via framework7-react and framework7-svelte packages. The wrapper ensures router.back() triggers framework-specific transition animations without breaking component lifecycle.
Operational Checks
Monitor these router events in production:
routeChangeStart/routeChangeEnd— measure transition latency. Target <100 ms on mid-tier mobile (Snapdragon 778G equivalent).routerError— fires when async component load fails. Attach an error boundary or fallback UI.- Hardware back button on Android must map to
router.back(). Test in Chrome DevTools device toolbar with "Emulate hardware back button" enabled.
Diagnostic snippet (run in browser console after app mount):
// Verify view count and history depth
console.log('Views:', app.router.views.map(v => ({
id: v.el.id,
historyLength: v.router.history.length,
currentRoute: v.router.currentRoute.url
})));
// Listen for async load failures
app.on('routerError', (err) => {
console.error('Route load failed:', err.route.path, err);
// Send to monitoring service
});
Expected check: each view's historyLength increments on forward navigation and decrements on back(). If two views share the same history array, you likely missed unique id attributes on the view elements.
Failure Modes
| Symptom | Root Cause | Mitigation |
|---|---|---|
| Infinite redirect loop to login | Guard re-checks expired token on /login/ itself |
Add if (to.path === '/login/') return resolve(); at guard start |
| Blank view after navigation | Async component import fails; no error boundary | Wrap lazy imports in try/catch; show fallback via routerError event |
| Tabs share history unexpectedly | Multiple view elements lack unique id or data-router |
Ensure each view has id="view-xxx" and data-router="true" |
| Mixed-content navigation fails silently on iOS 16+ | Cordova/Capacitor webview loads HTTPS app but navigates to HTTP asset | Enforce HTTPS in config.xml / capacitor.config.ts; test with webkit.debug console |
Conditions That Change the Design
- Micro-frontends: Multiple Framework7 app instances on one page share the global router event bus unless you namespace them. Set
app.params.router.eventsNamespace = 'app1-'per instance. - Legacy browser support: Framework7 v8+ requires Vite/webpack for async route components. For IE11 or old WebViews, transpile dynamic import to SystemJS or use pre-bundled routes (no code-splitting).
- Offline-first PWA: Add a service worker that caches route chunks. The router's
routerErrorevent can trigger a retry-from-cache strategy. - Deep linking from native: Universal Links / App Links must resolve to a route before the app mounts. Initialize the router with
url: window.location.pathnamein the Framework7 config.
Verification Checklist
- Create minimal v8 project:
npm init framework7@latest -- --framework=vue --bundler=vite - Add two async routes with
beforeEnterguards. - In Chrome DevTools device toolbar, test hardware back button maps to
router.back(). - Inspect
app.router.viewsarray length matches initialized views. - Simulate auth expiry: set short token TTL, navigate to guarded route, wait for expiry, attempt navigation—confirm single redirect to login without loop.
- Load test: 50 async components, measure transition latency via Performance API; target <100 ms.
Limitations
This architecture assumes Framework7 v8+ with a modern bundler. The router does not enforce HTTPS or CSP—those are deployment concerns. Nested view routers without unique IDs will share history unexpectedly; always verify view.router.history independence in console. Complex object passing via query string has practical URL length limits (~2 KB); use a client-side store for larger payloads.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.