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
25 changes: 25 additions & 0 deletions e2e/react-start/vitest-hooks/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "tanstack-react-start-e2e-vitest-hooks",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"test:e2e": "vitest run"
},
"dependencies": {
"@tanstack/react-router": "workspace:*",
"@tanstack/react-start": "workspace:*",
"react": "^19.2.3",
"react-dom": "^19.2.3"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"jsdom": "^27.4.0",
"typescript": "^5.9.3",
"vitest": "^4.0.16"
}
}
6 changes: 6 additions & 0 deletions e2e/react-start/vitest-hooks/src/components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useState } from 'react'

export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
}
68 changes: 68 additions & 0 deletions e2e/react-start/vitest-hooks/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* eslint-disable */

// @ts-nocheck

// noinspection JSUnusedGlobalSymbols

// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'

const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/'
fileRoutesByTo: FileRoutesByTo
to: '/'
id: '__root__' | '/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}
10 changes: 10 additions & 0 deletions e2e/react-start/vitest-hooks/src/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function getRouter() {
const router = createRouter({
routeTree,
})

return router
}
17 changes: 17 additions & 0 deletions e2e/react-start/vitest-hooks/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as React from 'react'
import { Outlet, createRootRoute } from '@tanstack/react-router'

export const Route = createRootRoute({
component: RootComponent,
})

function RootComponent() {
return (
<html>
<head />
<body>
<Outlet />
</body>
</html>
)
}
15 changes: 15 additions & 0 deletions e2e/react-start/vitest-hooks/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createFileRoute } from '@tanstack/react-router'
import { Counter } from '../components/Counter'

export const Route = createFileRoute('/')({
component: Home,
})

function Home() {
return (
<div>
<h1>Home</h1>
<Counter />
</div>
)
}
1 change: 1 addition & 0 deletions e2e/react-start/vitest-hooks/tests/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom'
21 changes: 21 additions & 0 deletions e2e/react-start/vitest-hooks/tests/vitest.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { Counter } from '../src/components/Counter'

/**
* This test verifies that the tanstackStart() vite plugin works correctly
* with Vitest. Without the fix in PR #6074, the optimizeDeps configuration
* would cause React to be pre-bundled incorrectly, leading to "Invalid hook
* call" errors when rendering components that use React hooks.
*
* The fix adds `process.env.VITEST !== 'true'` to disable optimizeDeps
* in Vitest environments.
*/
describe('Vitest with tanstackStart() plugin', () => {
it('renders a component using React hooks without errors', () => {
// This test will fail with "Invalid hook call" or similar React errors
// if optimizeDeps is incorrectly enabled in Vitest environment
render(<Counter />)
expect(screen.getByRole('button')).toHaveTextContent('Count: 0')
})
})
19 changes: 19 additions & 0 deletions e2e/react-start/vitest-hooks/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"isolatedModules": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"target": "ES2022",
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"noEmit": true
}
}
12 changes: 12 additions & 0 deletions e2e/react-start/vitest-hooks/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import { defineConfig } from 'vitest/config'

export default defineConfig({
plugins: [tanstackStart(), viteReact()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
},
})
14 changes: 10 additions & 4 deletions packages/react-start/src/plugin/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,16 @@ export function tanstackStart(
: ['@tanstack/react-router', '@tanstack/react-router-devtools'],
},
optimizeDeps:
environmentName === VITE_ENVIRONMENT_NAMES.client ||
(environmentName === VITE_ENVIRONMENT_NAMES.server &&
// This indicates that the server environment has opted in to dependency optimization
options.optimizeDeps?.noDiscovery === false)
// Vitest uses Vite's config, but `optimizeDeps` (pre-bundling) causes conflicts
// when workspace packages (transformed by Vitest) interact with external packages (Node-native).
// This is especially problematic for React, leading to "Invalid hook call" errors due to
// duplicate React instances (one pre-bundled ESM, one native CJS).
// We disable this enforced optimization when running in Vitest to allow standard resolution.
process.env.VITEST !== 'true' &&
(environmentName === VITE_ENVIRONMENT_NAMES.client ||
(environmentName === VITE_ENVIRONMENT_NAMES.server &&
// This indicates that the server environment has opted in to dependency optimization
options.optimizeDeps?.noDiscovery === false))
? {
// As `@tanstack/react-start` depends on `@tanstack/react-router`, we should exclude both.
exclude: [
Expand Down
Loading
Loading