diff --git a/packages/react-router/src/Asset.tsx b/packages/react-router/src/Asset.tsx
index 5e4f159f52..7f6297732b 100644
--- a/packages/react-router/src/Asset.tsx
+++ b/packages/react-router/src/Asset.tsx
@@ -270,7 +270,17 @@ function Script({
// After hydration, return null — the useEffect handles imperative injection.
if (!hydrated) {
if (attrs?.src) {
- return
+ if (!preventScriptHoist) {
+ return
+ }
+
+ return (
+
+ )
}
if (typeof children === 'string') {
diff --git a/packages/react-router/tests/Scripts.test.tsx b/packages/react-router/tests/Scripts.test.tsx
index 893cef01bc..e5c9dbbacc 100644
--- a/packages/react-router/tests/Scripts.test.tsx
+++ b/packages/react-router/tests/Scripts.test.tsx
@@ -8,6 +8,7 @@ import {
waitFor,
} from '@testing-library/react'
import { createPortal } from 'react-dom'
+import { hydrateRoot } from 'react-dom/client'
import ReactDOMServer from 'react-dom/server'
import { hydrate } from '@tanstack/router-core/ssr/client'
import { dehydrateSsrMatchId } from '../../router-core/src/ssr/ssr-match-id'
@@ -281,6 +282,82 @@ describe('scripts with async/defer attributes', () => {
expect(html).not.toContain('client-entry')
})
+ test('hydration honors preventScriptHoist for manifest src scripts', async () => {
+ const clientEntryAsset = {
+ attrs: {
+ src: '/hydrate-entry.js',
+ type: 'module',
+ async: true,
+ },
+ } satisfies NonNullable[number]
+
+ const createTestRouter = (isServer: boolean) => {
+ const rootRoute = createRootRoute({
+ component: () => {
+ return (
+
+ content
+
+
+
+ )
+ },
+ })
+
+ const indexRoute = createRoute({
+ path: '/',
+ getParentRoute: () => rootRoute,
+ })
+
+ const router = createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/'],
+ }),
+ routeTree: rootRoute.addChildren([indexRoute]),
+ isServer,
+ })
+ router.ssr = {
+ manifest: {
+ routes: {
+ [rootRoute.id]: {
+ scripts: [clientEntryAsset],
+ },
+ },
+ },
+ }
+ return router
+ }
+
+ const serverRouter = createTestRouter(true)
+ await serverRouter.load()
+
+ const html = ReactDOMServer.renderToString(
+ ,
+ )
+ expect(html).toContain('src="/hydrate-entry.js"')
+
+ const container = document.createElement('div')
+ container.innerHTML = html
+ document.body.appendChild(container)
+
+ const clientRouter = createTestRouter(false)
+ await clientRouter.load()
+
+ const onRecoverableError = vi.fn()
+ let root: ReturnType | undefined
+ await act(() => {
+ root = hydrateRoot(container, , {
+ onRecoverableError,
+ })
+ })
+
+ expect(await screen.findByTestId('hydrate-content')).toBeInTheDocument()
+ expect(onRecoverableError).not.toHaveBeenCalled()
+
+ await act(() => root?.unmount())
+ container.remove()
+ })
+
test('client renders scripts with attributes (including async/defer)', async () => {
const rootRoute = createRootRoute({
scripts: () => [