Using Framework7’s Per‑View Router and Page Lifecycle Hooks for Reliable SPA Navigation
Learn how to give each Framework7 tab its own router stack and use page lifecycle hooks to keep navigation state isolated and prevent stale event listeners.
20 Jul 2026, 01:45 UTC

The concrete problem: navigation state leaks across tabs
In a typical mobile layout you have a bottom toolbar with tabs: Home, Search, Profile. Each tab should remember its own navigation history. If all tabs share a single Framework7 View, switching tabs removes the previous tab’s pages from the DOM and the browser history no longer matches what the user sees. Manipulating the DOM outside Framework7’s page lifecycle makes the situation worse: event listeners attached to elements that Framework7 later caches or removes stay alive, causing memory leaks and handlers firing on the wrong page.
How per‑View routers and lifecycle hooks solve it
Framework7 lets you create multiple View instances, each with its own router stack. The router uses the History API (pushState/replaceState) to keep the address bar in sync with the visible page without a full reload, so the back and forward buttons work as expected. Each page component receives a page object where you can hook into lifecycle events: pageInit runs after the page is inserted, pageBeforeRemove runs before the page is taken out, and pageRemoved runs after removal. Placing data fetching, listener attachment, or timer starts in pageInit and cleanup in pageBeforeRemove guarantees that work is tied to the page’s lifetime.
Worked example: two independent tabs with their own router stacks
The following snippet shows how to initialise a Framework7 app, create a separate View for each tab, and attach page‑lifecycle handlers.
// app.js – run after the Framework7 library is loaded
const app = new Framework7({
root: '#app',
routes: [
{ path: '/', componentUrl: './pages/home.html' },
{ path: '/search/', componentUrl: './pages/search.html' },
{ path: '/profile/', componentUrl: './pages/profile.html' }
]
});
// Create a view for the main tab (home)
const homeView = app.views.create('#home-view', {
router: true,
url: '/'
});
// Create a view for the search tab
const searchView = app.views.create('#search-view', {
router: true,
url: '/search/'
});
// Create a view for the profile tab
const profileView = app.views.create('#profile-view', {
router: true,
url: '/profile/'
});
// Page‑lifecycle handlers
app.on('pageInit', (page) => {
if (page.name === 'search') {
console.log('search page init – fetch data and bind listeners');
// Example: fetch data and attach a click listener
// fetch('/api/items').then(r => r.json()).then(data => {
// renderList(data);
// document.getElementById('refresh-btn').addEventListener('click', refresh);
// });
}
});
app.on('pageBeforeRemove', (page) => {
if (page.name === 'search') {
console.log('search page beforeRemove – clean up');
// Example: remove listeners and cancel timers
// document.getElementById('refresh-btn').removeEventListener('click', refresh);
// clearInterval(timerId);
}
});
Place each tab’s content inside its corresponding container (#home-view, #search-view, #profile-view) so that navigation links are routed to that view’s stack. When you navigate from Home to Search, the URL changes via the History API, the Search page is fetched via AJAX (if you use componentUrl), and the pageInit handler runs. Pressing the browser back button returns to Home, triggering pageBeforeRemove on the Search page and pageInit on Home again.
Trade‑off and limitation
Framework7 can load pages automatically with AJAX and render them using Template7. This reduces boilerplate but adds a network request for each new page, which can increase perceived load time on slow connections. AJAX loading also requires the server to send appropriate CORS headers if the API is on a different origin. For offline‑first applications you must cache the AJAX responses yourself (e.g., with Service Workers) because Framework7 does not do it automatically.
Another limitation is that Framework7 keeps pages in its DOM cache after they are removed. If you rely on DOM state (such as form input values) persisting between visits, you must reset that state in pageInit rather than assuming a fresh DOM.
Actionable closing
- Audit your code for direct DOM queries or global event listeners. Move setup into
pageInitand teardown intopageBeforeRemove. - Ensure each tab, side panel, or modal that needs its own history uses a separate View with
router: true. - After navigating, open the browser devtools console and inspect the router object, e.g.,
f7.views.homeView.router. Verify that thehistoryarray matches the sequence of URLs you visited. - Check that
pageInitandpageBeforeRemovelogs appear in the expected order when you go forward and back.
By giving each navigation context its own router stack and tying all work to Framework7’s page lifecycle, you keep URLs in sync, prevent stale handlers, and make the back button behave predictably.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.