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
12 changes: 12 additions & 0 deletions e2e/react-router/react-compiler/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Compiler useMatchRoute test</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
29 changes: 29 additions & 0 deletions e2e/react-router/react-compiler/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "tanstack-router-e2e-react-compiler",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"dev:e2e": "vite",
"build": "vite build && tsc --noEmit",
"preview": "vite preview",
"start": "vite",
"test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
},
"dependencies": {
"@tanstack/react-router": "workspace:^",
Comment thread
Sheraff marked this conversation as resolved.
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@playwright/test": "^1.61.0",
"@rolldown/plugin-babel": "^0.2.0",
"@tanstack/router-e2e-utils": "workspace:^",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^6.0.1",
"babel-plugin-react-compiler": "^1.0.0",
"vite": "^8.0.14"
}
}
25 changes: 25 additions & 0 deletions e2e/react-router/react-compiler/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { defineConfig, devices } from '@playwright/test'
import { getTestServerPort } from '@tanstack/router-e2e-utils'
import packageJson from './package.json' with { type: 'json' }

const PORT = await getTestServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`

export default defineConfig({
testDir: './tests',
workers: 1,
reporter: [['line']],
use: { baseURL },
webServer: {
command: `VITE_NODE_ENV="test" VITE_SERVER_PORT=${PORT} pnpm dev:e2e --port ${PORT}`,
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
})
73 changes: 73 additions & 0 deletions e2e/react-router/react-compiler/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import {
Link,
Outlet,
RouterProvider,
createRootRoute,
createRoute,
createRouter,
linkOptions,
useMatchRoute,
} from '@tanstack/react-router'

const links = linkOptions([
{ to: '/home', label: 'Home' },
{ to: '/about', label: 'About' },
])

function useRouteName() {
const matchRoute = useMatchRoute()

return links.find((link) => matchRoute(link))?.label ?? 'Unknown'
}

function RootComponent() {
const matchedRoute = useRouteName()

return (
<>
<nav>
{links.map((link) => (
<Link key={link.label} {...link}>
{link.label}
</Link>
))}
</nav>
<p>
Matched route: <span data-testid="matched-route">{matchedRoute}</span>
</p>
<Outlet />
</>
)
}

const rootRoute = createRootRoute({ component: RootComponent })
const homeRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/home',
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
})
const router = createRouter({
routeTree: rootRoute.addChildren([homeRoute, aboutRoute]),
})

declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}

const rootElement = document.getElementById('app')
if (!rootElement) {
throw new Error('Root element not found')
}

createRoot(rootElement).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)
16 changes: 16 additions & 0 deletions e2e/react-router/react-compiler/tests/use-match-route.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { expect, test } from '@playwright/test'

test('useMatchRoute updates after navigation with React Compiler', async ({
page,
}) => {
await page.goto('/home')
await expect(page.getByTestId('matched-route')).toHaveText('Home')

await page.getByRole('link', { name: 'About' }).click()
await expect(page).toHaveURL(/\/about$/)
await expect(page.getByTestId('matched-route')).toHaveText('About')

await page.getByRole('link', { name: 'Home' }).click()
await expect(page).toHaveURL(/\/home$/)
await expect(page.getByTestId('matched-route')).toHaveText('Home')
})
15 changes: 15 additions & 0 deletions e2e/react-router/react-compiler/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"target": "ESNext",
"moduleResolution": "Bundler",
"module": "ESNext",
"resolveJsonModule": true,
"allowJs": true,
"skipLibCheck": true,
"types": ["vite/client"]
},
"exclude": ["node_modules", "dist"]
}
7 changes: 7 additions & 0 deletions e2e/react-router/react-compiler/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import babel from '@rolldown/plugin-babel'

export default defineConfig({
plugins: [react(), babel({ presets: [reactCompilerPreset()] })],
})
25 changes: 15 additions & 10 deletions packages/react-router/src/Matches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,6 @@ export type UseMatchRouteOptions<
export function useMatchRoute<TRouter extends AnyRouter = RegisteredRouter>() {
const router = useRouter()

if (!(isServer ?? router.isServer)) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useStore(router.stores.location, (location) => location.href)
// eslint-disable-next-line react-hooks/rules-of-hooks
useStore(router.stores.resolvedLocation, (location) => location?.href)
// eslint-disable-next-line react-hooks/rules-of-hooks
useStore(router.stores.status, (status) => status)
}

return React.useCallback(
<
const TFrom extends string = string,
Expand All @@ -183,7 +174,21 @@ export function useMatchRoute<TRouter extends AnyRouter = RegisteredRouter>() {
includeSearch,
})
},
[router],
// eslint-disable-next-line react-hooks/exhaustive-deps
(isServer ?? router.isServer)
? [router]
: [
router,
// eslint-disable-next-line react-hooks/rules-of-hooks
useStore(router.stores.location, (location) => location.href),
// eslint-disable-next-line react-hooks/rules-of-hooks
useStore(
router.stores.resolvedLocation,
(location) => location?.href,
),
// eslint-disable-next-line react-hooks/rules-of-hooks
useStore(router.stores.status, (status) => status),
],
)
}

Expand Down
55 changes: 55 additions & 0 deletions packages/react-router/tests/Matches.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
cleanup,
fireEvent,
render,
renderHook,
screen,
waitFor,
} from '@testing-library/react'
Expand All @@ -12,6 +13,7 @@ import { createControlledPromise } from '@tanstack/router-core'
import {
Link,
Outlet,
RouterContextProvider,
RouterProvider,
createRootRoute,
createRoute,
Expand Down Expand Up @@ -232,6 +234,59 @@ test('useMatchRoute follows superseding pending locations', async () => {
})
})

test('useMatchRoute callback identity tracks route matching state', async () => {
const root = createRootRoute()
const a = createRoute({
getParentRoute: () => root,
path: '/a',
})
const b = createRoute({
getParentRoute: () => root,
path: '/b',
})
const router = createRouter({
routeTree: root.addChildren([a, b]),
history: createMemoryHistory({ initialEntries: ['/'] }),
})
const aLocation = router.buildLocation({ to: '/a' })
const bLocation = router.buildLocation({ to: '/b' })

const { result, rerender } = renderHook(() => useMatchRoute(), {
wrapper: ({ children }) => (
<RouterContextProvider router={router}>{children}</RouterContextProvider>
),
})

let previous = result.current
rerender()
expect(result.current).toBe(previous)

async function expectCallbackInvalidated(update: () => void) {
previous = result.current
act(update)
await waitFor(() => expect(result.current).not.toBe(previous))
}

await expectCallbackInvalidated(() => {
router.stores.location.set(aLocation)
})
await expectCallbackInvalidated(() => {
router.stores.resolvedLocation.set(bLocation)
})
await expectCallbackInvalidated(() => {
router.stores.status.set('pending')
})

previous = result.current
act(() => {
router.stores.location.set({ ...aLocation })
router.stores.resolvedLocation.set({ ...bLocation })
router.stores.status.set('pending')
})
rerender()
expect(result.current).toBe(previous)
})

test('legacy notFoundRoute drops a stale parent layout after navigation', async () => {
let legacyLoads = 0
const root = createRootRoute({ component: Outlet })
Expand Down
Loading
Loading