Skip to content

Commit 7dda4cd

Browse files
committed
feat: new setting +keepAlive
New setting for preserving page component instances (and thus their state) across client-side navigation, by wrapping the <Page> component with Vue's built-in <KeepAlive> component. Wrapping <slot/> with <KeepAlive> inside a +Layout component cannot work: <KeepAlive> only caches its direct child component, whereas slot content is rendered as a Fragment which <KeepAlive> cannot cache. Thus vike-vue applies <KeepAlive> where it works: directly around the routed <Page> component. Usage: // pages/+config.ts export default { keepAlive: true, // or KeepAliveProps, e.g. { max: 10 } } See https://github.com/orgs/vikejs/discussions/3433 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MU13ncNmLi5rz2xySmm5rZ
1 parent 76fc9fd commit 7dda4cd

7 files changed

Lines changed: 73 additions & 1 deletion

File tree

examples/full/.testRun.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ function testRun(cmd: `pnpm run ${'dev' | 'preview'}`) {
4040

4141
testClientOnly()
4242

43+
testKeepAlive()
44+
4345
const textNoSSR = 'This page is rendered only in the browser'
4446
{
4547
const url = '/without-ssr'
@@ -295,6 +297,29 @@ function countMatches(haystack: string, needleRe: RegExp) {
295297
return (haystack.match(new RegExp(needleRe, 'g')) || []).length
296298
}
297299

300+
// https://vike.dev/keepAlive
301+
function testKeepAlive() {
302+
test('keepAlive - page state is preserved across client-side navigation', async () => {
303+
await page.goto(getServerUrl() + '/keep-alive/first')
304+
expect(await page.textContent('h1')).toBe('KeepAlive - First Page')
305+
await testCounter()
306+
307+
await page.click('a:has-text("KeepAlive - Second Page")')
308+
await autoRetry(async () => {
309+
expect(await page.textContent('h1')).toBe('KeepAlive - Second Page')
310+
})
311+
await ensureWasClientSideRouted('/pages/keep-alive/first')
312+
313+
await page.click('a:has-text("KeepAlive - First Page")')
314+
await autoRetry(async () => {
315+
expect(await page.textContent('h1')).toBe('KeepAlive - First Page')
316+
})
317+
await ensureWasClientSideRouted('/pages/keep-alive/first')
318+
// The counter didn't reset to 0: the page's component instance was preserved by <KeepAlive>
319+
expect(await page.textContent('button')).toContain('Counter 1')
320+
})
321+
}
322+
298323
function testClientOnly() {
299324
const url = '/client-only'
300325
const textLoading = 'Loading client-only component...'
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { Config } from 'vike/types'
2+
3+
// https://vike.dev/keepAlive
4+
export default {
5+
keepAlive: true,
6+
} satisfies Config
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<template>
2+
<h1>KeepAlive - First Page</h1>
3+
<p>This page's component instance is preserved across client-side navigation.</p>
4+
<Counter />
5+
<p><a href="/keep-alive/second">KeepAlive - Second Page</a></p>
6+
</template>
7+
8+
<script lang="ts" setup>
9+
import Counter from '../../../components/Counter.vue'
10+
</script>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<template>
2+
<h1>KeepAlive - Second Page</h1>
3+
<p>Navigate back to the first page: its state is preserved.</p>
4+
<p><a href="/keep-alive/first">KeepAlive - First Page</a></p>
5+
</template>

packages/vike-vue/src/+config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ const config = {
3838
env: { server: true, client: true },
3939
cumulative: true,
4040
},
41+
keepAlive: {
42+
env: { server: true, client: true },
43+
},
4144
title: {
4245
env: { server: true, client: true },
4346
},

packages/vike-vue/src/integration/createVueApp.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
createApp,
77
createSSRApp,
88
h,
9+
KeepAlive,
910
nextTick,
1011
shallowRef,
1112
shallowReactive,
@@ -33,11 +34,21 @@ async function createVueApp(
3334
if (entryComponentName === 'Page') {
3435
const entryComponentRef = shallowRef(pageContext.config[entryComponentName])
3536
const layoutRef = shallowRef(pageContext.config.Layout || [])
37+
const keepAliveRef = shallowRef(pageContext.config.keepAlive ?? false)
3638
onChangePage = (pageContext: PageContext) => {
3739
entryComponentRef.value = pageContext.config[entryComponentName]
3840
layoutRef.value = pageContext.config.Layout || []
41+
keepAliveRef.value = pageContext.config.keepAlive ?? false
42+
}
43+
// Wrap <Page> with <KeepAlive>
44+
// - It's the only way to make <KeepAlive> work: it cannot be applied inside a +Layout component
45+
// because <KeepAlive> only caches its direct child component, whereas <Layout> receives <Page>
46+
// as slot content (Vue renders slot content as a Fragment which <KeepAlive> cannot cache).
47+
const EntryComponent = () => {
48+
const keepAlive = keepAliveRef.value
49+
if (!keepAlive) return h(entryComponentRef.value)
50+
return h(KeepAlive, keepAlive === true ? null : keepAlive, () => h(entryComponentRef.value))
3951
}
40-
const EntryComponent = () => h(entryComponentRef.value)
4152
RootComponent = () => {
4253
let RootComp = EntryComponent
4354
layoutRef.value.forEach((layout) => {

packages/vike-vue/src/types/Config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { TagAttributes } from '../utils/getTagAttributesString.js'
33
import type { Viewport, HtmlInjection } from '../integration/onRenderHtml.js'
44
import type { ConfigsCumulative } from '../hooks/useConfig/configsCumulative.js'
55
import type { Component } from './PageContext.js'
6+
import type { KeepAliveProps } from 'vue'
67

78
// https://vike.dev/pageContext#typescript
89
declare global {
@@ -24,6 +25,17 @@ declare global {
2425
*/
2526
Layout?: Component
2627

28+
/**
29+
* Whether to wrap the `<Page>` component with Vue's built-in [`<KeepAlive>`](https://vuejs.org/guide/built-ins/keep-alive.html) component, preserving page component instances (and thus their state) across client-side navigation.
30+
*
31+
* Instead of `true`, you can provide `<KeepAlive>` props (e.g. `{ include, exclude, max }`) for controlling which page components are cached.
32+
*
33+
* @default false
34+
*
35+
* https://vike.dev/keepAlive
36+
*/
37+
keepAlive?: boolean | KeepAliveProps
38+
2739
/**
2840
* Set the page's tilte.
2941
*

0 commit comments

Comments
 (0)