Managing Complex SPA Navigation with Framework7's View-Based Router
Framework7's view-based router lets you run multiple independent navigation stacks in a single SPA — ideal for sidebars, tabs, and master-detail layouts. This post shows how to set up dual views, handle route parameters, add auth guards, and avoid memory bloat.
05 Nov 2025, 18:07 UTC

The Problem: When a Single Navigation Stack Isn't Enough
Most single-page applications start with a simple linear history: the user moves forward and back through a single stack of pages. But real products quickly outgrow that model. A dashboard with a persistent sidebar, a chat app with a conversation list and an active thread, or an e-commerce site with a filter panel that needs its own back button — all of these demand multiple independent navigation histories that coexist on the same screen.
Framework7's Router solves this by treating views as first-class navigation containers. Each view maintains its own history stack, its own current route, and its own transition animations. The main view might show a product list while a side-panel view shows filter options; pressing the browser's back button pops the main view's stack, while a close button in the panel pops the panel's stack. This article walks through setting up that pattern, handling route parameters, and the trade-offs you'll hit when the view count grows.
Core Concept: Views Are Independent Routers
A Framework7 app is initialized with one main view by default, but you can create as many additional views as you need. Each view gets its own router instance, its own routes configuration (or a shared one), and its own DOM container. Navigation calls like router.navigate('/user/42') only affect the view they're called on.
This separation means you can load a detail page in the main view while the side panel stays on its filter page. The two histories never interfere. When the user clicks the browser back button, Framework7 asks the active view to go back; if that view is at its root, the framework can optionally delegate to another view or fall back to the browser's default behavior.
Worked Example: Master-Detail with a Filter Panel
Imagine a product catalog. The left column (main view) shows a list. Tapping a product pushes a detail page. The right column (panel view) shows filter categories; tapping a category pushes a sub-filter page. Both columns need independent back navigation.
// app.js
const app = new Framework7({
el: '#app',
routes: [
{ path: '/', component: ProductList },
{ path: '/product/:id', component: ProductDetail },
{ path: '/filters/', component: FilterList },
{ path: '/filters/:category', component: FilterDetail }
],
view: {
// default main view
iosDynamicNavbar: true,
materialDynamicNavbar: true
}
});
// Create the panel view after app init
const panelView = app.views.create('#panel-view', {
routes: [
{ path: '/filters/', component: FilterList },
{ path: '/filters/:category', component: FilterDetail }
],
url: '/filters/' // initial route for this view
});
// In ProductList component, navigate main view:
this.$f7router.navigate(`/product/${productId}`);
// In FilterList component, navigate panel view:
panelView.router.navigate(`/filters/${category}`);
Each view's router parses its own URL. In ProductDetail, the parameter is available as this.$route.params.id. In FilterDetail, it's this.$route.params.category. The two views can even share the same route definitions if you pass the same routes array, but keeping them separate avoids accidental cross-view matches.
Route Guards with beforeEnter
Authentication checks belong in beforeEnter hooks. Because each view has its own router, you can attach guards per-view or globally. A common pattern: protect the main view's detail routes while leaving the panel view open.
routes: [
{
path: '/product/:id',
component: ProductDetail,
beforeEnter: (to, from, resolve) => {
if (!app.user.authenticated) {
resolve({ path: '/login', query: { redirect: to.url } });
} else {
resolve(true);
}
}
}
]
The resolve callback accepts true to proceed, a redirect object, or false to cancel. Since the guard runs on the target view's router, a login redirect in the main view won't touch the panel view's history.
Trade-Off: Memory and Deep-Linking Complexity
Every live view keeps its pages in the DOM unless you explicitly unload them. With three or four views, each holding five to ten pages, you can accumulate hundreds of components — event listeners, timers, and bound data — that never get garbage-collected. Framework7 provides router.back() and router.clearPreviousHistory(), but you must call them deliberately.
Deep linking adds another wrinkle. When a user refreshes /product/42?filter=color, the server must serve index.html for any path so the client-side router can boot and reconstruct both views' states. A typical nginx rule:
location / {
try_files $uri $uri/ /index.html;
}
Then, in your app's init, you read the initial URL and programmatically navigate each view to its correct route. If the panel view should start at /filters/color, you call panelView.router.navigate('/filters/color', { history: false }) before the first render. Forgetting this step leaves the panel stuck at its default route while the main view shows the deep-linked product.
Verification Checklist
- Open the app, navigate three levels deep in the main view, then two levels in the panel. Press the browser back button — only the main view should pop.
- Refresh the page at a deep-linked URL. Both views should restore their correct routes without a 404.
- Log
this.$route.paramsin each component'smounted()hook to confirm parameter parsing. - Profile memory after 20 navigations per view; ensure detached components are actually unloaded (no retained event listeners).
Closing: Start with Two Views, Measure Before Adding More
Framework7's view-based router is powerful because it maps directly to UI patterns that users already understand: tabs, sidebars, modals with their own history. Start by isolating the one part of your UI that needs independent back navigation — usually a sidebar or a bottom tab bar — and promote it to its own view. Add beforeEnter guards for auth, configure your web server for deep links, and run the verification steps above. Only introduce a third view when you have a measured need and a plan for cleanup. The router scales, but your memory budget doesn't.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.