Skip to content

Commit 8860728

Browse files
committed
refactor(router-view): fix instances without a key
1 parent ed0a108 commit 8860728

File tree

3 files changed

+93
-32
lines changed

3 files changed

+93
-32
lines changed

e2e/guards-instances/index.ts

+43-7
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import {
33
createWebHistory,
44
onBeforeRouteUpdate,
55
onBeforeRouteLeave,
6+
useRoute,
7+
useRouter,
68
} from '../../src'
7-
import { createApp, ref, reactive, defineComponent } from 'vue'
9+
import { createApp, ref, reactive, defineComponent, computed } from 'vue'
810

911
// override existing style on dev with shorter times
1012
if (!__CI__) {
@@ -42,9 +44,9 @@ const state = reactive({
4244
const Foo = defineComponent({
4345
template: '<div>foo {{ enterCallback }}</div>',
4446
data: () => ({ key: 'Foo', enterCallback: 0 }),
45-
mounted() {
46-
console.log('mounted Foo')
47-
},
47+
// mounted() {
48+
// console.log('mounted Foo')
49+
// },
4850
beforeRouteEnter(to, from, next) {
4951
state.enter++
5052
logs.value.push(`enter ${from.path} - ${to.path}`)
@@ -91,6 +93,29 @@ const router = createRouter({
9193
],
9294
})
9395

96+
// preserve existing query
97+
const originalPush = router.push
98+
router.push = to => {
99+
if (typeof to === 'string') {
100+
const resolved = router.resolve(to)
101+
return router.push({
102+
path: to,
103+
query: {
104+
testCase: router.currentRoute.value.query.testCase,
105+
...resolved.query,
106+
},
107+
})
108+
} else {
109+
return originalPush({
110+
...to,
111+
query: {
112+
testCase: router.currentRoute.value.query.testCase,
113+
...to.query,
114+
},
115+
})
116+
}
117+
}
118+
94119
const app = createApp({
95120
template: `
96121
<div id="app">
@@ -136,9 +161,9 @@ leaves: {{ state.leave }}
136161
<router-view :key="$route.query.foo" class="view" />
137162
</template>
138163
<template v-else-if="testCase === 'keepalivekeyed'">
139-
<router-view v-slot="{ Component, key }" >
164+
<router-view v-slot="{ Component }" >
140165
<keep-alive>
141-
<component :is="Component" :key="$route.query.foo || key" class="view" />
166+
<component :is="Component" :key="$route.query.foo" class="view" />
142167
</keep-alive>
143168
</router-view>
144169
</template>
@@ -149,7 +174,18 @@ leaves: {{ state.leave }}
149174
</div>
150175
`,
151176
setup() {
152-
const testCase = ref('')
177+
const router = useRouter()
178+
const route = useRoute()
179+
180+
const testCase = computed<string>({
181+
get: () => {
182+
let { testCase } = route.query
183+
return !testCase || Array.isArray(testCase) ? '' : testCase
184+
},
185+
set(testCase) {
186+
router.push({ query: { ...route.query, testCase } })
187+
},
188+
})
153189

154190
return { state, logs, testCase }
155191
},

e2e/specs/guards-instances.js

+20-14
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ function testCase(browser) {
55
.click('li:nth-child(2) a')
66
.assert.containsText('.view', 'foo 1')
77
.click('li:nth-child(3) a')
8-
.assert.containsText('.view', 'foo 1')
8+
.assert.containsText('.view', 'foo 2')
99
.click('li:nth-child(4) a')
10-
.assert.containsText('.view', 'foo 1')
10+
.assert.containsText('.view', 'foo 2')
1111
.click('li:nth-child(5) a')
12-
.assert.containsText('.view', 'foo 1')
12+
.assert.containsText('.view', 'foo 2')
1313
.click('li:nth-child(2) a')
14-
.assert.containsText('.view', 'foo')
15-
.assert.containsText(
16-
'#logs',
14+
.assert.containsText('.view', 'foo 3')
15+
.expect.element('#logs')
16+
.text.to.equal(
1717
[
1818
'enter / - /foo',
1919
'leave /foo - /f/1',
@@ -68,20 +68,22 @@ module.exports = {
6868
.click('li:nth-child(1) a')
6969
// keep alive keeps the correct instance
7070
.click('li:nth-child(2) a')
71-
.assert.containsText('.view', 'foo 3')
71+
.expect.element('.view')
72+
.text.equals('foo 4')
73+
browser
7274
.click('li:nth-child(1) a')
7375
.click('li:nth-child(2) a')
74-
.assert.containsText('.view', 'foo 4')
76+
.assert.containsText('.view', 'foo 5')
7577
.click('li:nth-child(3) a')
76-
.assert.containsText('.view', 'foo 2')
78+
.assert.containsText('.view', 'foo 6')
7779
// leave the update view and enter it again
7880
.click('li:nth-child(1) a')
7981
.click('li:nth-child(3) a')
8082
.click('#resetLogs')
8183
.click('li:nth-child(4) a')
8284
.click('li:nth-child(1) a')
83-
.assert.containsText(
84-
'#logs',
85+
.expect.element('#logs')
86+
.text.to.equal(
8587
[
8688
'update /f/1 - /f/2',
8789
'setup:update /f/1 - /f/2',
@@ -146,12 +148,12 @@ module.exports = {
146148
.click('li:nth-child(1) a')
147149
// keep alive keeps the correct instance
148150
.click('li:nth-child(2) a')
149-
.assert.containsText('.view', 'foo 3')
151+
.assert.containsText('.view', 'foo 4')
150152
.click('li:nth-child(1) a')
151153
.click('li:nth-child(2) a')
152-
.assert.containsText('.view', 'foo 4')
154+
.assert.containsText('.view', 'foo 5')
153155
.click('li:nth-child(3) a')
154-
.assert.containsText('.view', 'foo 2')
156+
.assert.containsText('.view', 'foo 6')
155157

156158
.click('li:nth-child(5) a')
157159
// the query is used as a key resetting the enter count
@@ -161,6 +163,10 @@ module.exports = {
161163
.click('li:nth-child(6) a')
162164
.assert.containsText('.view', 'foo 1')
163165
.click('li:nth-child(5) a')
166+
.assert.containsText('.view', 'foo 6')
167+
// on reused instance
168+
.click('li:nth-child(2) a')
169+
.click('li:nth-child(6) a')
164170
.assert.containsText('.view', 'foo 2')
165171

166172
browser.end()

src/RouterView.ts

+30-11
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
computed,
1212
AllowedComponentProps,
1313
ComponentCustomProps,
14+
watch,
1415
} from 'vue'
1516
import {
1617
RouteLocationNormalized,
@@ -24,6 +25,7 @@ import {
2425
} from './injectionSymbols'
2526
import { assign } from './utils'
2627
import { warn } from './warning'
28+
import { isSameRouteRecord } from './location'
2729

2830
export interface RouterViewProps {
2931
name?: string
@@ -55,18 +57,42 @@ export const RouterViewImpl = defineComponent({
5557

5658
const viewRef = ref<ComponentPublicInstance>()
5759

60+
watch(
61+
() => [viewRef.value, matchedRouteRef.value, props.name] as const,
62+
([instance, to, name], [oldInstance, from, oldName]) => {
63+
// copy reused instances
64+
if (to) {
65+
to.instances[name] = instance
66+
// the component instance is reused for a different route or name
67+
if (from && instance === oldInstance) {
68+
to.leaveGuards = from.leaveGuards
69+
to.updateGuards = from.updateGuards
70+
}
71+
}
72+
73+
if (
74+
instance &&
75+
to &&
76+
(!from || !isSameRouteRecord(to, from) || !oldInstance)
77+
) {
78+
;(to.enterCallbacks[name] || []).forEach(callback =>
79+
callback(viewRef.value!)
80+
)
81+
}
82+
}
83+
)
84+
5885
return () => {
5986
const route = props.route || injectedRoute
6087
const matchedRoute = matchedRouteRef.value
6188
const ViewComponent = matchedRoute && matchedRoute.components[props.name]
6289
// we need the value at the time we render because when we unmount, we
6390
// navigated to a different location so the value is different
6491
const currentName = props.name
65-
const key = matchedRoute && currentName + matchedRoute.path
6692

6793
if (!ViewComponent) {
6894
return slots.default
69-
? slots.default({ Component: ViewComponent, route, key })
95+
? slots.default({ Component: ViewComponent, route })
7096
: null
7197
}
7298

@@ -80,21 +106,14 @@ export const RouterViewImpl = defineComponent({
80106
: routePropsOption
81107
: null
82108

83-
const onVnodeMounted = () => {
84-
matchedRoute!.instances[currentName] = viewRef.value
85-
;(matchedRoute!.enterCallbacks[currentName] || []).forEach(callback =>
86-
callback(viewRef.value!)
87-
)
88-
}
89109
const onVnodeUnmounted = () => {
90110
// remove the instance reference to prevent leak
91111
matchedRoute!.instances[currentName] = null
92112
}
93113

94114
const component = h(
95115
ViewComponent,
96-
assign({ key }, routeProps, attrs, {
97-
onVnodeMounted,
116+
assign({}, routeProps, attrs, {
98117
onVnodeUnmounted,
99118
ref: viewRef,
100119
})
@@ -104,7 +123,7 @@ export const RouterViewImpl = defineComponent({
104123
// pass the vnode to the slot as a prop.
105124
// h and <component :is="..."> both accept vnodes
106125
slots.default
107-
? slots.default({ Component: component, route, key })
126+
? slots.default({ Component: component, route })
108127
: component
109128
)
110129
}

0 commit comments

Comments
 (0)