diff --git a/.changeset/twenty-tools-lose.md b/.changeset/twenty-tools-lose.md new file mode 100644 index 00000000000..73b079ad1c0 --- /dev/null +++ b/.changeset/twenty-tools-lose.md @@ -0,0 +1,5 @@ +--- +'@tanstack/solid-router': patch +--- + +Enhanced internal tag comparison logic to optimize stylesheet persistence during route navigation. diff --git a/packages/solid-router/src/headContentUtils.tsx b/packages/solid-router/src/headContentUtils.tsx index 853daa5c3bf..5af5012e9f3 100644 --- a/packages/solid-router/src/headContentUtils.tsx +++ b/packages/solid-router/src/headContentUtils.tsx @@ -2,7 +2,6 @@ import * as Solid from 'solid-js' import { escapeHtml, getAssetCrossOrigin, - replaceEqualDeep, resolveManifestAssetLink, } from '@tanstack/router-core' import { useRouter } from './useRouter' @@ -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, + next: Array, +) { + const prevByKey = new Map() + 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(arr: Array, fn: (item: T) => string) { const seen = new Set() return arr.filter((item) => { diff --git a/packages/solid-router/tests/Scripts.test.tsx b/packages/solid-router/tests/Scripts.test.tsx index 0153965de66..29f3138352d 100644 --- a/packages/solid-router/tests/Scripts.test.tsx +++ b/packages/solid-router/tests/Scripts.test.tsx @@ -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 ( + <> + + + + ) + }, + }) + + const aRoute = createRoute({ + path: '/a', + getParentRoute: () => rootRoute, + component: () => Go to B, + }) + + const bRoute = createRoute({ + path: '/b', + getParentRoute: () => rootRoute, + component: () => Go to A, + }) + + 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(() => ) + + 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()