Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as PostsRouteImport } from './routes/posts'
import { Route as LayoutRouteImport } from './routes/_layout'
import { Route as IndexRouteImport } from './routes/index'
import { Route as TransitionIndexRouteImport } from './routes/transition/index'
import { Route as PostsIndexRouteImport } from './routes/posts.index'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
Expand All @@ -32,6 +33,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const TransitionIndexRoute = TransitionIndexRouteImport.update({
id: '/transition/',
path: '/transition/',
getParentRoute: () => rootRouteImport,
} as any)
const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
Expand Down Expand Up @@ -62,13 +68,15 @@ export interface FileRoutesByFullPath {
'/posts': typeof PostsRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
'/posts/': typeof PostsIndexRoute
'/transition': typeof TransitionIndexRoute
'/layout-a': typeof LayoutLayout2LayoutARoute
'/layout-b': typeof LayoutLayout2LayoutBRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/posts/$postId': typeof PostsPostIdRoute
'/posts': typeof PostsIndexRoute
'/transition': typeof TransitionIndexRoute
'/layout-a': typeof LayoutLayout2LayoutARoute
'/layout-b': typeof LayoutLayout2LayoutBRoute
}
Expand All @@ -80,6 +88,7 @@ export interface FileRoutesById {
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
'/posts/': typeof PostsIndexRoute
'/transition/': typeof TransitionIndexRoute
'/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
'/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
}
Expand All @@ -90,10 +99,17 @@ export interface FileRouteTypes {
| '/posts'
| '/posts/$postId'
| '/posts/'
| '/transition'
| '/layout-a'
| '/layout-b'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/posts/$postId' | '/posts' | '/layout-a' | '/layout-b'
to:
| '/'
| '/posts/$postId'
| '/posts'
| '/transition'
| '/layout-a'
| '/layout-b'
id:
| '__root__'
| '/'
Expand All @@ -102,6 +118,7 @@ export interface FileRouteTypes {
| '/_layout/_layout-2'
| '/posts/$postId'
| '/posts/'
| '/transition/'
| '/_layout/_layout-2/layout-a'
| '/_layout/_layout-2/layout-b'
fileRoutesById: FileRoutesById
Expand All @@ -110,6 +127,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
LayoutRoute: typeof LayoutRouteWithChildren
PostsRoute: typeof PostsRouteWithChildren
TransitionIndexRoute: typeof TransitionIndexRoute
}

declare module '@tanstack/react-router' {
Expand All @@ -135,6 +153,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/transition/': {
id: '/transition/'
path: '/transition'
fullPath: '/transition'
preLoaderRoute: typeof TransitionIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/posts/': {
id: '/posts/'
path: '/'
Expand Down Expand Up @@ -214,6 +239,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
LayoutRoute: LayoutRouteWithChildren,
PostsRoute: PostsRouteWithChildren,
TransitionIndexRoute: TransitionIndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Link, createFileRoute } from '@tanstack/react-router'
import { Suspense, useMemo } from 'react'
import { queryOptions, useQuery } from '@tanstack/react-query'
import { z } from 'zod'

const searchSchema = z.object({
n: z.number().default(1),
})

const doubleQueryOptions = (n: number) =>
queryOptions({
queryKey: ['transition-double', n],
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return n * 2
},
placeholderData: (oldData) => oldData,
})

export const Route = createFileRoute('/transition/')({
validateSearch: searchSchema,
loader: ({ context: { queryClient }, location }) => {
const { n } = searchSchema.parse(location.search)
return queryClient.ensureQueryData(doubleQueryOptions(n))
},
component: TransitionPage,
})

function TransitionPage() {
const search = Route.useSearch()

const doubleQuery = useMemo(
() => useQuery(doubleQueryOptions(search.n)),
[search.n],
)

return (
<Suspense fallback="Loading...">
<div className="p-2">
<Link
data-testid="increase-button"
className="border bg-gray-50 px-3 py-1"
from="/transition"
search={(s) => ({ n: s.n + 1 })}
>
Increase
</Link>

<div className="mt-2">
<div data-testid="n-value">n: {search.n}</div>
<div data-testid="double-value">double: {doubleQuery.data}</div>
</div>
</div>
</Suspense>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect, test } from '@playwright/test'

test('react-query transitions keep previous data during navigation', async ({
page,
}) => {
await page.goto('/transition')

await expect(page.getByTestId('n-value')).toContainText('n: 1')
await expect(page.getByTestId('double-value')).toContainText('double: 2')

const bodySnapshots: Array<string> = []

const interval = setInterval(async () => {
const text = await page
.locator('body')
.textContent()
.catch(() => '')
if (text) bodySnapshots.push(text)
}, 50)

await page.getByTestId('increase-button').click()

await page.waitForTimeout(200)

clearInterval(interval)

await expect(page.getByTestId('n-value')).toContainText('n: 2', {
timeout: 2_000,
})
await expect(page.getByTestId('double-value')).toContainText('double: 4', {
timeout: 2_000,
})

const sawLoading = bodySnapshots.some((text) => text.includes('Loading...'))

expect(sawLoading).toBeFalsy()
})
21 changes: 21 additions & 0 deletions e2e/solid-router/basic-file-based/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Route as LayoutRouteImport } from './routes/_layout'
import { Route as SearchParamsRouteRouteImport } from './routes/search-params/route'
import { Route as NonNestedRouteRouteImport } from './routes/non-nested/route'
import { Route as IndexRouteImport } from './routes/index'
import { Route as TransitionIndexRouteImport } from './routes/transition/index'
import { Route as SearchParamsIndexRouteImport } from './routes/search-params/index'
import { Route as RelativeIndexRouteImport } from './routes/relative/index'
import { Route as RedirectIndexRouteImport } from './routes/redirect/index'
Expand Down Expand Up @@ -158,6 +159,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const TransitionIndexRoute = TransitionIndexRouteImport.update({
id: '/transition/',
path: '/transition/',
getParentRoute: () => rootRouteImport,
} as any)
const SearchParamsIndexRoute = SearchParamsIndexRouteImport.update({
id: '/',
path: '/',
Expand Down Expand Up @@ -600,6 +606,7 @@ export interface FileRoutesByFullPath {
'/redirect': typeof RedirectIndexRoute
'/relative': typeof RelativeIndexRoute
'/search-params/': typeof SearchParamsIndexRoute
'/transition': typeof TransitionIndexRoute
'/non-nested/named/$baz': typeof NonNestedNamedBazRouteRouteWithChildren
'/non-nested/path/baz': typeof NonNestedPathBazRouteRouteWithChildren
'/non-nested/prefix/prefix{$baz}': typeof NonNestedPrefixPrefixChar123bazChar125RouteRouteWithChildren
Expand Down Expand Up @@ -684,6 +691,7 @@ export interface FileRoutesByTo {
'/redirect': typeof RedirectIndexRoute
'/relative': typeof RelativeIndexRoute
'/search-params': typeof SearchParamsIndexRoute
'/transition': typeof TransitionIndexRoute
'/params-ps/named/$foo': typeof ParamsPsNamedFooRouteRouteWithChildren
'/params-ps/non-nested/$foo': typeof ParamsPsNonNestedFooRouteRouteWithChildren
'/insidelayout': typeof groupLayoutInsidelayoutRoute
Expand Down Expand Up @@ -771,6 +779,7 @@ export interface FileRoutesById {
'/redirect/': typeof RedirectIndexRoute
'/relative/': typeof RelativeIndexRoute
'/search-params/': typeof SearchParamsIndexRoute
'/transition/': typeof TransitionIndexRoute
'/non-nested/named/$baz': typeof NonNestedNamedBazRouteRouteWithChildren
'/non-nested/path/baz': typeof NonNestedPathBazRouteRouteWithChildren
'/non-nested/prefix/prefix{$baz}': typeof NonNestedPrefixPrefixChar123bazChar125RouteRouteWithChildren
Expand Down Expand Up @@ -860,6 +869,7 @@ export interface FileRouteTypes {
| '/redirect'
| '/relative'
| '/search-params/'
| '/transition'
| '/non-nested/named/$baz'
| '/non-nested/path/baz'
| '/non-nested/prefix/prefix{$baz}'
Expand Down Expand Up @@ -944,6 +954,7 @@ export interface FileRouteTypes {
| '/redirect'
| '/relative'
| '/search-params'
| '/transition'
| '/params-ps/named/$foo'
| '/params-ps/non-nested/$foo'
| '/insidelayout'
Expand Down Expand Up @@ -1030,6 +1041,7 @@ export interface FileRouteTypes {
| '/redirect/'
| '/relative/'
| '/search-params/'
| '/transition/'
| '/non-nested/named/$baz'
| '/non-nested/path/baz'
| '/non-nested/prefix/prefix{$baz}'
Expand Down Expand Up @@ -1112,6 +1124,7 @@ export interface RootRouteChildren {
ParamsPsIndexRoute: typeof ParamsPsIndexRoute
RedirectIndexRoute: typeof RedirectIndexRoute
RelativeIndexRoute: typeof RelativeIndexRoute
TransitionIndexRoute: typeof TransitionIndexRoute
ParamsPsNamedFooRouteRoute: typeof ParamsPsNamedFooRouteRouteWithChildren
groupSubfolderInsideRoute: typeof groupSubfolderInsideRoute
ParamsPsNamedPrefixChar123fooChar125Route: typeof ParamsPsNamedPrefixChar123fooChar125Route
Expand Down Expand Up @@ -1216,6 +1229,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/transition/': {
id: '/transition/'
path: '/transition'
fullPath: '/transition'
preLoaderRoute: typeof TransitionIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/search-params/': {
id: '/search-params/'
path: '/'
Expand Down Expand Up @@ -2104,6 +2124,7 @@ const rootRouteChildren: RootRouteChildren = {
ParamsPsIndexRoute: ParamsPsIndexRoute,
RedirectIndexRoute: RedirectIndexRoute,
RelativeIndexRoute: RelativeIndexRoute,
TransitionIndexRoute: TransitionIndexRoute,
ParamsPsNamedFooRouteRoute: ParamsPsNamedFooRouteRouteWithChildren,
groupSubfolderInsideRoute: groupSubfolderInsideRoute,
ParamsPsNamedPrefixChar123fooChar125Route:
Expand Down
48 changes: 48 additions & 0 deletions e2e/solid-router/basic-file-based/src/routes/transition/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Link, createFileRoute } from '@tanstack/solid-router'
import { Suspense, createResource } from 'solid-js'
import { z } from 'zod'

export const Route = createFileRoute('/transition/')({
validateSearch: z.object({
n: z.number().default(1),
}),
component: Home,
})

function Home() {
return (
<div class="p-2">
<Link
data-testid="increase-button"
class="border bg-gray-50 px-3 py-1"
from="/transition"
search={(s) => ({ n: s.n + 1 })}
>
Increase
</Link>

<Result />
</div>
)
}

function Result() {
const searchQuery = Route.useSearch()

const [doubleQuery] = createResource(
() => searchQuery().n,
async (n) => {
await new Promise((r) => setTimeout(r, 1000))
return n * 2
},
)

return (
<div class="mt-2">
<Suspense fallback="Loading...">
<div data-testid="n-value">n: {searchQuery().n}</div>
<div data-testid="double-value">double: {doubleQuery()}</div>
</Suspense>
</div>
)
}
43 changes: 43 additions & 0 deletions e2e/solid-router/basic-file-based/tests/transition.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { expect, test } from '@playwright/test'

test('transitions should keep old values visible during navigation', async ({
page,
}) => {
await page.goto('/transition')

await expect(page.getByTestId('n-value')).toContainText('n: 1')
await expect(page.getByTestId('double-value')).toContainText('double: 2')

const bodyTexts: Array<string> = []

const pollInterval = setInterval(async () => {
const text = await page
.locator('body')
.textContent()
.catch(() => '')
if (text) bodyTexts.push(text)
}, 50)

await page.getByTestId('increase-button').click()

await page.waitForTimeout(200)

clearInterval(pollInterval)

await expect(page.getByTestId('n-value')).toContainText('n: 2', {
timeout: 2000,
})
await expect(page.getByTestId('double-value')).toContainText('double: 4', {
timeout: 2000,
})

// With proper transitions, old values should remain visible until new ones arrive
const hasLoadingText = bodyTexts.some((text) => text.includes('Loading...'))

if (hasLoadingText) {
throw new Error(
'FAILED: "Loading..." appeared during navigation. ' +
'Solid Router should use transitions to keep old values visible.',
)
}
})
Loading
Loading