Skip to content

Commit 35d4ec3

Browse files
committed
Merge branch 'main' into start-clerk-basic-solid
2 parents 1bea099 + 1b75808 commit 35d4ec3

File tree

184 files changed

+5496
-440
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

184 files changed

+5496
-440
lines changed

docs/start/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,10 @@
338338
"label": "Basic + Supabase",
339339
"to": "framework/solid/examples/start-basic-supabase"
340340
},
341+
{
342+
"label": "Bare + Convex + Better Auth",
343+
"to": "framework/solid/examples/start-convex-better-auth"
344+
},
341345
{
342346
"label": "Cloudflare Vite Plugin",
343347
"to": "framework/solid/examples/start-basic-cloudflare"

docs/start/framework/react/guide/execution-model.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,31 @@ function Analytics() {
9898
}
9999
```
100100

101+
#### useHydrated Hook
102+
103+
For more granular control over hydration-dependent behavior, use the `useHydrated` hook. It returns a boolean indicating whether the client has been hydrated:
104+
105+
```tsx
106+
import { useHydrated } from '@tanstack/react-router'
107+
108+
function TimeZoneDisplay() {
109+
const hydrated = useHydrated()
110+
const timeZone = hydrated
111+
? Intl.DateTimeFormat().resolvedOptions().timeZone
112+
: 'UTC'
113+
114+
return <div>Your timezone: {timeZone}</div>
115+
}
116+
```
117+
118+
**Behavior:**
119+
120+
- **During SSR**: Always returns `false`
121+
- **First client render**: Returns `false`
122+
- **After hydration**: Returns `true` (and stays `true` for all subsequent renders)
123+
124+
This is useful when you need to conditionally render content based on client-side data (like browser timezone, locale, or localStorage) while providing a sensible fallback for server rendering.
125+
101126
### Environment-Specific Implementations
102127

103128
```tsx

docs/start/framework/react/guide/hosting.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ However, since hosting is one of the most crucial aspects of your application's
1818
1919
Once you've chosen a deployment target, you can follow the deployment guidelines below to deploy your TanStack Start application to the hosting provider of your choice:
2020

21-
- [`cloudflare-workers`](#cloudflare-workers): Deploy to Cloudflare Workers
21+
- [`cloudflare-workers`](#cloudflare-workers--official-partner): Deploy to Cloudflare Workers
2222
- [`netlify`](#netlify): Deploy to Netlify
2323
- [`nitro`](#nitro): Deploy using Nitro
2424
- [`vercel`](#vercel): Deploy to Vercel

docs/start/framework/solid/guide/execution-model.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,30 @@ function Analytics() {
9898
}
9999
```
100100

101+
#### useHydrated Hook
102+
103+
For more granular control over hydration-dependent behavior, use the `useHydrated` hook. It returns an accessor (signal) indicating whether the client has been hydrated:
104+
105+
```tsx
106+
import { useHydrated } from '@tanstack/solid-router'
107+
108+
function TimeZoneDisplay() {
109+
const hydrated = useHydrated()
110+
const timeZone = () =>
111+
hydrated() ? Intl.DateTimeFormat().resolvedOptions().timeZone : 'UTC'
112+
113+
return <div>Your timezone: {timeZone()}</div>
114+
}
115+
```
116+
117+
**Behavior:**
118+
119+
- **During SSR**: Always returns `false`
120+
- **First client render**: Returns `false`
121+
- **After hydration**: Returns `true` (and stays `true` for all subsequent renders)
122+
123+
This is useful when you need to conditionally render content based on client-side data (like browser timezone, locale, or localStorage) while providing a sensible fallback for server rendering.
124+
101125
### Environment-Specific Implementations
102126

103127
```tsx

e2e/react-router/basic-react-query-file-based/src/routeTree.gen.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import { Route as rootRouteImport } from './routes/__root'
1212
import { Route as PostsRouteImport } from './routes/posts'
1313
import { Route as LayoutRouteImport } from './routes/_layout'
1414
import { Route as IndexRouteImport } from './routes/index'
15-
import { Route as TransitionIndexRouteImport } from './routes/transition/index'
1615
import { Route as PostsIndexRouteImport } from './routes/posts.index'
1716
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
1817
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
18+
import { Route as TransitionCountQueryRouteImport } from './routes/transition/count/query'
1919
import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
2020
import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
2121

@@ -33,11 +33,6 @@ const IndexRoute = IndexRouteImport.update({
3333
path: '/',
3434
getParentRoute: () => rootRouteImport,
3535
} as any)
36-
const TransitionIndexRoute = TransitionIndexRouteImport.update({
37-
id: '/transition/',
38-
path: '/transition/',
39-
getParentRoute: () => rootRouteImport,
40-
} as any)
4136
const PostsIndexRoute = PostsIndexRouteImport.update({
4237
id: '/',
4338
path: '/',
@@ -52,6 +47,11 @@ const LayoutLayout2Route = LayoutLayout2RouteImport.update({
5247
id: '/_layout-2',
5348
getParentRoute: () => LayoutRoute,
5449
} as any)
50+
const TransitionCountQueryRoute = TransitionCountQueryRouteImport.update({
51+
id: '/transition/count/query',
52+
path: '/transition/count/query',
53+
getParentRoute: () => rootRouteImport,
54+
} as any)
5555
const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({
5656
id: '/layout-b',
5757
path: '/layout-b',
@@ -68,17 +68,17 @@ export interface FileRoutesByFullPath {
6868
'/posts': typeof PostsRouteWithChildren
6969
'/posts/$postId': typeof PostsPostIdRoute
7070
'/posts/': typeof PostsIndexRoute
71-
'/transition': typeof TransitionIndexRoute
7271
'/layout-a': typeof LayoutLayout2LayoutARoute
7372
'/layout-b': typeof LayoutLayout2LayoutBRoute
73+
'/transition/count/query': typeof TransitionCountQueryRoute
7474
}
7575
export interface FileRoutesByTo {
7676
'/': typeof IndexRoute
7777
'/posts/$postId': typeof PostsPostIdRoute
7878
'/posts': typeof PostsIndexRoute
79-
'/transition': typeof TransitionIndexRoute
8079
'/layout-a': typeof LayoutLayout2LayoutARoute
8180
'/layout-b': typeof LayoutLayout2LayoutBRoute
81+
'/transition/count/query': typeof TransitionCountQueryRoute
8282
}
8383
export interface FileRoutesById {
8484
__root__: typeof rootRouteImport
@@ -88,9 +88,9 @@ export interface FileRoutesById {
8888
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
8989
'/posts/$postId': typeof PostsPostIdRoute
9090
'/posts/': typeof PostsIndexRoute
91-
'/transition/': typeof TransitionIndexRoute
9291
'/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
9392
'/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
93+
'/transition/count/query': typeof TransitionCountQueryRoute
9494
}
9595
export interface FileRouteTypes {
9696
fileRoutesByFullPath: FileRoutesByFullPath
@@ -99,17 +99,17 @@ export interface FileRouteTypes {
9999
| '/posts'
100100
| '/posts/$postId'
101101
| '/posts/'
102-
| '/transition'
103102
| '/layout-a'
104103
| '/layout-b'
104+
| '/transition/count/query'
105105
fileRoutesByTo: FileRoutesByTo
106106
to:
107107
| '/'
108108
| '/posts/$postId'
109109
| '/posts'
110-
| '/transition'
111110
| '/layout-a'
112111
| '/layout-b'
112+
| '/transition/count/query'
113113
id:
114114
| '__root__'
115115
| '/'
@@ -118,16 +118,16 @@ export interface FileRouteTypes {
118118
| '/_layout/_layout-2'
119119
| '/posts/$postId'
120120
| '/posts/'
121-
| '/transition/'
122121
| '/_layout/_layout-2/layout-a'
123122
| '/_layout/_layout-2/layout-b'
123+
| '/transition/count/query'
124124
fileRoutesById: FileRoutesById
125125
}
126126
export interface RootRouteChildren {
127127
IndexRoute: typeof IndexRoute
128128
LayoutRoute: typeof LayoutRouteWithChildren
129129
PostsRoute: typeof PostsRouteWithChildren
130-
TransitionIndexRoute: typeof TransitionIndexRoute
130+
TransitionCountQueryRoute: typeof TransitionCountQueryRoute
131131
}
132132

133133
declare module '@tanstack/react-router' {
@@ -153,13 +153,6 @@ declare module '@tanstack/react-router' {
153153
preLoaderRoute: typeof IndexRouteImport
154154
parentRoute: typeof rootRouteImport
155155
}
156-
'/transition/': {
157-
id: '/transition/'
158-
path: '/transition'
159-
fullPath: '/transition'
160-
preLoaderRoute: typeof TransitionIndexRouteImport
161-
parentRoute: typeof rootRouteImport
162-
}
163156
'/posts/': {
164157
id: '/posts/'
165158
path: '/'
@@ -181,6 +174,13 @@ declare module '@tanstack/react-router' {
181174
preLoaderRoute: typeof LayoutLayout2RouteImport
182175
parentRoute: typeof LayoutRoute
183176
}
177+
'/transition/count/query': {
178+
id: '/transition/count/query'
179+
path: '/transition/count/query'
180+
fullPath: '/transition/count/query'
181+
preLoaderRoute: typeof TransitionCountQueryRouteImport
182+
parentRoute: typeof rootRouteImport
183+
}
184184
'/_layout/_layout-2/layout-b': {
185185
id: '/_layout/_layout-2/layout-b'
186186
path: '/layout-b'
@@ -239,7 +239,7 @@ const rootRouteChildren: RootRouteChildren = {
239239
IndexRoute: IndexRoute,
240240
LayoutRoute: LayoutRouteWithChildren,
241241
PostsRoute: PostsRouteWithChildren,
242-
TransitionIndexRoute: TransitionIndexRoute,
242+
TransitionCountQueryRoute: TransitionCountQueryRoute,
243243
}
244244
export const routeTree = rootRouteImport
245245
._addFileChildren(rootRouteChildren)

e2e/react-router/basic-react-query-file-based/src/routes/transition/index.tsx renamed to e2e/react-router/basic-react-query-file-based/src/routes/transition/count/query.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Link, createFileRoute } from '@tanstack/react-router'
2-
import { Suspense, useMemo } from 'react'
2+
import { Suspense } from 'react'
33
import { queryOptions, useQuery } from '@tanstack/react-query'
44
import { z } from 'zod'
55

@@ -12,12 +12,12 @@ const doubleQueryOptions = (n: number) =>
1212
queryKey: ['transition-double', n],
1313
queryFn: async () => {
1414
await new Promise((resolve) => setTimeout(resolve, 1000))
15-
return n * 2
15+
return { n, double: n * 2 }
1616
},
1717
placeholderData: (oldData) => oldData,
1818
})
1919

20-
export const Route = createFileRoute('/transition/')({
20+
export const Route = createFileRoute('/transition/count/query')({
2121
validateSearch: searchSchema,
2222
loader: ({ context: { queryClient }, location }) => {
2323
const { n } = searchSchema.parse(location.search)
@@ -37,15 +37,17 @@ function TransitionPage() {
3737
<Link
3838
data-testid="increase-button"
3939
className="border bg-gray-50 px-3 py-1"
40-
from="/transition"
40+
from="/transition/count/query"
4141
search={(s) => ({ n: s.n + 1 })}
4242
>
4343
Increase
4444
</Link>
4545

4646
<div className="mt-2">
47-
<div data-testid="n-value">n: {search.n}</div>
48-
<div data-testid="double-value">double: {doubleQuery.data}</div>
47+
<div data-testid="n-value">n: {doubleQuery.data?.n}</div>
48+
<div data-testid="double-value">
49+
double: {doubleQuery.data?.double}
50+
</div>
4951
</div>
5052
</div>
5153
</Suspense>
Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,95 @@
11
import { expect, test } from '@playwright/test'
22

3-
test('react-query transitions keep previous data during navigation', async ({
3+
test('transitions/count/query should keep old values visible during navigation', async ({
44
page,
55
}) => {
6-
await page.goto('/transition')
6+
await page.goto('/transition/count/query')
77

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

11-
const bodySnapshots: Array<string> = []
11+
const bodyTexts: Array<string> = []
1212

13-
const interval = setInterval(async () => {
13+
const pollInterval = setInterval(async () => {
1414
const text = await page
1515
.locator('body')
1616
.textContent()
1717
.catch(() => '')
18-
if (text) bodySnapshots.push(text)
18+
if (text) bodyTexts.push(text)
1919
}, 50)
2020

21-
await page.getByTestId('increase-button').click()
21+
// 1 click
22+
23+
page.getByTestId('increase-button').click()
24+
25+
await expect(page.getByTestId('n-value')).toContainText('n: 1', {
26+
timeout: 2_000,
27+
})
28+
await expect(page.getByTestId('double-value')).toContainText('double: 2', {
29+
timeout: 2_000,
30+
})
2231

2332
await page.waitForTimeout(200)
2433

25-
clearInterval(interval)
34+
await expect(page.getByTestId('n-value')).toContainText('n: 2', {
35+
timeout: 2000,
36+
})
37+
await expect(page.getByTestId('double-value')).toContainText('double: 4', {
38+
timeout: 2000,
39+
})
40+
41+
// 2 clicks
42+
43+
page.getByTestId('increase-button').click()
44+
page.getByTestId('increase-button').click()
2645

2746
await expect(page.getByTestId('n-value')).toContainText('n: 2', {
28-
timeout: 2_000,
47+
timeout: 2000,
2948
})
3049
await expect(page.getByTestId('double-value')).toContainText('double: 4', {
31-
timeout: 2_000,
50+
timeout: 2000,
51+
})
52+
53+
await page.waitForTimeout(200)
54+
55+
await expect(page.getByTestId('n-value')).toContainText('n: 4', {
56+
timeout: 2000,
57+
})
58+
await expect(page.getByTestId('double-value')).toContainText('double: 8', {
59+
timeout: 2000,
60+
})
61+
62+
// 3 clicks
63+
64+
page.getByTestId('increase-button').click()
65+
page.getByTestId('increase-button').click()
66+
page.getByTestId('increase-button').click()
67+
68+
await expect(page.getByTestId('n-value')).toContainText('n: 4', {
69+
timeout: 2000,
3270
})
71+
await expect(page.getByTestId('double-value')).toContainText('double: 8', {
72+
timeout: 2000,
73+
})
74+
75+
await page.waitForTimeout(200)
76+
77+
await expect(page.getByTestId('n-value')).toContainText('n: 7', {
78+
timeout: 2000,
79+
})
80+
await expect(page.getByTestId('double-value')).toContainText('double: 14', {
81+
timeout: 2000,
82+
})
83+
84+
clearInterval(pollInterval)
3385

34-
const sawLoading = bodySnapshots.some((text) => text.includes('Loading...'))
86+
// With proper transitions, old values should remain visible until new ones arrive
87+
const hasLoadingText = bodyTexts.some((text) => text.includes('Loading...'))
3588

36-
expect(sawLoading).toBeFalsy()
89+
if (hasLoadingText) {
90+
throw new Error(
91+
'FAILED: "Loading..." appeared during navigation. ' +
92+
'Solid Router should use transitions to keep old values visible.',
93+
)
94+
}
3795
})

e2e/react-start/basic-react-query/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"react-dom": "^19.0.0",
2222
"redaxios": "^0.5.1",
2323
"tailwind-merge": "^2.6.0",
24-
"vite": "^7.1.7"
24+
"vite": "^7.1.7",
25+
"zod": "^4.1.12"
2526
},
2627
"devDependencies": {
2728
"@playwright/test": "^1.50.1",

0 commit comments

Comments
 (0)