Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cyan-beers-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/vue-router': patch
---

Remove an unnecessary match Fragment to reduce VNode allocation and transient JIT compilation memory while preserving route content and scroll restoration.
8 changes: 3 additions & 5 deletions packages/vue-router/src/Match.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,9 @@ export const Match = Vue.defineComponent({

return Vue.h(Vue.Fragment, null, [
content,
Vue.h(Vue.Fragment, null, [
(isServer ?? router.isServer) && router.options.scrollRestoration
? Vue.h(ScrollRestoration)
: null,
]),
(isServer ?? router.isServer) && router.options.scrollRestoration
? Vue.h(ScrollRestoration)
: null,
])
}

Expand Down
107 changes: 107 additions & 0 deletions packages/vue-router/tests/Match.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import * as Vue from 'vue'
import { renderToString } from 'vue/server-renderer'
import { afterEach, expect, test, vi } from 'vitest'
import { cleanup, fireEvent, render } from '@testing-library/vue'
import {
Outlet,
RouterProvider,
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
} from '../src'

afterEach(cleanup)

function createTestRouter(
scrollRestoration: boolean | (() => boolean),
isServer = false,
) {
const revision = Vue.ref(0)
const mounted = vi.fn()
const Child = Vue.defineComponent({
setup() {
const count = Vue.ref(0)
Vue.onMounted(mounted)
return () => <button onClick={() => count.value++}>{count.value}</button>
},
})
const Shell = Vue.defineComponent({
setup(_, { slots }) {
return () => (
<div data-testid="shell" data-revision={revision.value}>
{slots.default?.()}
</div>
)
},
})
const root = createRootRoute({
shellComponent: Shell,
component: () => (
<main>
<Outlet />
</main>
),
})
const child = createRoute({
getParentRoute: () => root,
path: '/child',
component: Child,
})
const router = createRouter({
routeTree: root.addChildren([child]),
history: createMemoryHistory({ initialEntries: ['/child'] }),
isServer,
scrollRestoration,
ssr: { nonce: 'test-nonce' },
})
return { router, revision, mounted }
}

test.each([false, true])(
'preserves root child state through shell updates (scroll restoration: %s)',
async (scrollRestoration) => {
const { router, revision, mounted } = createTestRouter(scrollRestoration)
const app = render(<RouterProvider router={router} />)
const button = await app.findByRole('button')

await fireEvent.click(button)
revision.value++
router.update({ scrollRestoration: !scrollRestoration })
await router.invalidate()
await Vue.nextTick()

expect(app.getByRole('button')).toBe(button)
expect(button).toHaveTextContent('1')
expect(mounted).toHaveBeenCalledOnce()
expect(app.getByTestId('shell')).toHaveAttribute('data-revision', '1')
},
)

test.each([
['disabled', false, false],
['enabled', true, true],
['skipped by selector', (): boolean => false, false],
['enabled by selector', (): boolean => true, true],
] as const)(
'renders root child content and the restoration script on the server (%s)',
async (_, scrollRestoration, hasScript) => {
const { router } = createTestRouter(scrollRestoration, true)
await router.load()
const html = await renderToString(
Vue.createSSRApp(() => <RouterProvider router={router} />),
)
const container = document.createElement('div')
container.innerHTML = html

expect(container.querySelector('main > button')).toHaveTextContent('0')
const scripts = container.querySelectorAll('main > script')
expect(scripts).toHaveLength(hasScript ? 1 : 0)
if (hasScript) {
expect(scripts[0]).toHaveAttribute('nonce', 'test-nonce')
expect(scripts[0]?.textContent).toContain(
'document.currentScript.remove()',
)
}
},
)
Loading