Preserve iframe State Across Vue Route Changes Without Reloading
Vue’s built-in keep-alive caches component VNodes and instances, but it cannot prevent an iframe’s document from reloading. An iframe hosts an independant browsing context; each time its DOM node is mounted, the embedded page is loaded anew. Caching the VNode does not capture the iframe’s internal document, and re-rendering the iframe element is functionally equivalent to opening a new page.
To keep iframe state intact during route navigation, avoid remounting the iframe node. Render iframe components outside of router-view and toggle visibility with v-show so the DOM node persists across route changes.
Baseline pattern
Define routes for iframe paths without assigning a component so router-view does not mount anything for those paths. Render iframe components next to router-view and control visibility with v-show.
main.js:
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'
Vue.use(VueRouter)
const Home = { template: '<div>Home</div>' }
const routeTable = [
// No component: handled outside router-view
{ path: '/frame-a', name: 'frame-a' },
{ path: '/frame-b', name: 'frame-b' },
// Normal routed view
{ path: '/home', component: Home }
]
const router = new VueRouter({ routes: routeTable })
new Vue({
router,
render: h => h(App)
}).$mount('#app')
App.vue:
<template>
<div id="app">
<nav class="nav">
<router-link class="link" to="/frame-a">Frame A</router-link>
<router-link class="link" to="/frame-b">Frame B</router-link>
<router-link class="link" to="/home">Home</router-link>
</nav>
<keep-alive>
<router-view />
</keep-alive>
<!-- Persist iframe DOM nodes; switch visibility only -->
<FrameA v-show="$route.path === '/frame-a'" />
<FrameB v-show="$route.path === '/frame-b'" />
</div>
</template>
<script>
import FrameA from './components/FrameA.vue'
import FrameB from './components/FrameB.vue'
export default {
name: 'App',
components: { FrameA, FrameB }
}
</script>
Key idea:
- Do not let router-view mount the iframe routes.
- Mount iframe components once at the root and toggle thier visibility with v-show to keep their internal state alive.
Generalized and lazy approach
The baseline works but requires manual imports and always renders iframe nodes at app start. Improve it by:
- Marking iframe routes in the route config with a dedicated field holding the component.
- Dynamically rendering only iframe components the user has visited (lazy mount).
- Ecnapsulating the switching logic in to a custom router-view wrapper.
Route configuration
Attach the iframe component to the route record:
main.js:
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'
import FrameA from './components/FrameA.vue'
import FrameB from './components/FrameB.vue'
Vue.use(VueRouter)
const routes = [
{ path: '/frame-a', name: 'frame-a', iframeComponent: FrameA },
{ path: '/frame-b', name: 'frame-b', iframeComponent: FrameB },
{ path: '/home', component: { template: '<div>Home</div>' } }
]
const router = new VueRouter({ routes })
new Vue({ router, render: h => h(App) }).$mount('#app')
Smart router-view wrapper
Create a component that:
- Renders normal routes through keep-alive + router-view.
- Builds a list of iframe routes from the router config.
- Lazily mounts an iframe component when its route is first visited and keeps it in the DOM, toggling visibility by path.
SmartRouterView.vue:
<template>
<div>
<keep-alive>
<router-view />
</keep-alive>
<component
v-for="entry in mountedIframeEntries"
:key="entry.key"
:is="entry.component"
v-show="$route.path === entry.path"
/>
</div>
</template>
<script>
export default {
data() {
return {
iframeEntries: [] // { key, path, opened, component }
}
},
created() {
this.iframeEntries = this.collectIframeEntries()
this.markOpened(this.$route)
},
watch: {
$route(to) {
this.markOpened(to)
}
},
computed: {
mountedIframeEntries() {
return this.iframeEntries.filter(e => e.opened)
}
},
methods: {
collectIframeEntries() {
const routes = (this.$router && this.$router.options && this.$router.options.routes) || []
return routes
.filter(r => r.iframeComponent)
.map(r => ({
key: r.name || r.path,
path: r.path,
opened: false,
component: r.iframeComponent
}))
},
markOpened(route) {
const hit = this.iframeEntries.find(e => e.path === route.path)
if (hit && !hit.opened) hit.opened = true
}
}
}
</script>
Use the wrapper in App.vue:
<template>
<div id="app">
<nav class="nav">
<router-link class="link" to="/frame-a">Frame A</router-link>
<router-link class="link" to="/frame-b">Frame B</router-link>
<router-link class="link" to="/home">Home</router-link>
</nav>
<SmartRouterView />
</div>
</template>
<script>
import SmartRouterView from './components/SmartRouterView.vue'
export default {
name: 'App',
components: { SmartRouterView }
}
</script>
Notes:
- iframe routes omit a component in the route record so router-view skips mounting them. The wrapper renders and preserves iframe components independently.
- Lazy mounting is driven by the opened flag, set the first time the corresponding route becomes active.
- Visibility siwtching uses v-show so the iframe DOM nodes (and their internal browsing contexts) remain intact across route transitions.