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/twenty-tools-lose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/solid-router': patch
---

Enhanced internal tag comparison logic to optimize stylesheet persistence during route navigation.
29 changes: 27 additions & 2 deletions packages/solid-router/src/headContentUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as Solid from 'solid-js'
import {
escapeHtml,
getAssetCrossOrigin,
replaceEqualDeep,
resolveManifestAssetLink,
} from '@tanstack/router-core'
import { useRouter } from './useRouter'
Expand Down Expand Up @@ -211,10 +210,36 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => {
if (prev === undefined) {
return next
}
return replaceEqualDeep(prev, next)
return replaceEqualTags(prev, next)
})
}

function replaceEqualTags(
prev: Array<RouterManagedTag>,
next: Array<RouterManagedTag>,
) {
const prevByKey = new Map<string, RouterManagedTag>()
for (const tag of prev) {
prevByKey.set(JSON.stringify(tag), tag)
}

let isEqual = prev.length === next.length
const result = next.map((tag, index) => {
const existing = prevByKey.get(JSON.stringify(tag))
if (existing) {
if (existing !== prev[index]) {
isEqual = false
}
return existing
}

isEqual = false
return tag
})

return isEqual ? prev : result
}

export function uniqBy<T>(arr: Array<T>, fn: (item: T) => string) {
const seen = new Set<string>()
return arr.filter((item) => {
Expand Down
91 changes: 91 additions & 0 deletions packages/solid-router/tests/Scripts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,97 @@ describe('ssr scripts', () => {
).toHaveLength(1)
})

test('keeps manifest stylesheet links mounted when preload counts change', async () => {
const history = createTestBrowserHistory()

const rootRoute = createRootRoute({
component: () => {
return (
<>
<HeadContent />
<Outlet />
</>
)
},
})

const aRoute = createRoute({
path: '/a',
getParentRoute: () => rootRoute,
component: () => <Link to="/b">Go to B</Link>,
})

const bRoute = createRoute({
path: '/b',
getParentRoute: () => rootRoute,
component: () => <Link to="/a">Go to A</Link>,
})

const router = createRouter({
history,
routeTree: rootRoute.addChildren([aRoute, bRoute]),
})

router.ssr = {
manifest: {
routes: {
[rootRoute.id]: {
preloads: ['/root.js'],
assets: [
{
tag: 'link',
attrs: {
rel: 'stylesheet',
href: '/main.css',
},
},
],
},
[aRoute.id]: {
preloads: ['/a.js'],
assets: [],
},
[bRoute.id]: {
preloads: ['/b.js', '/b-child.js'],
assets: [],
},
},
},
}

await router.navigate({ to: '/a' })
await router.load()

render(() => <RouterProvider router={router} />)

const getStylesheetLink = () =>
Array.from(document.head.querySelectorAll('link[rel="stylesheet"]')).find(
(link) => link.getAttribute('href') === '/main.css',
)

await waitFor(() => {
expect(getStylesheetLink()).toBeInstanceOf(HTMLLinkElement)
})

const initialLink = getStylesheetLink()
expect(initialLink).toBeInstanceOf(HTMLLinkElement)

fireEvent.click(screen.getByRole('link', { name: 'Go to B' }))

await waitFor(() => {
expect(router.state.location.pathname).toBe('/b')
})

await screen.findByRole('link', { name: 'Go to A' })

expect(getStylesheetLink()).toBe(initialLink)
expect(
Array.from(
document.head.querySelectorAll('link[rel="stylesheet"]'),
).filter((link) => link.getAttribute('href') === '/main.css'),
).toHaveLength(1)
})

test('applies assetCrossOrigin to manifest assets and preloads', async () => {
const history = createTestBrowserHistory()

Expand Down
Loading