@tanstack/react-router 1.159.14 → 1.160.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Asset.cjs +33 -19
- package/dist/cjs/Asset.cjs.map +1 -1
- package/dist/cjs/index.cjs +4 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/index.dev.cjs +4 -0
- package/dist/cjs/index.dev.cjs.map +1 -1
- package/dist/cjs/link.cjs +6 -6
- package/dist/cjs/link.cjs.map +1 -1
- package/dist/esm/Asset.js +33 -19
- package/dist/esm/Asset.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.dev.js +2 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/link.js +6 -6
- package/dist/esm/link.js.map +1 -1
- package/dist/llms/rules/api.d.ts +1 -1
- package/dist/llms/rules/api.js +35 -0
- package/dist/llms/rules/guide.d.ts +1 -1
- package/dist/llms/rules/guide.js +2821 -253
- package/dist/llms/rules/installation.d.ts +1 -1
- package/dist/llms/rules/installation.js +376 -357
- package/dist/llms/rules/routing.d.ts +1 -1
- package/dist/llms/rules/routing.js +436 -31
- package/dist/llms/rules/setup-and-architecture.d.ts +1 -1
- package/dist/llms/rules/setup-and-architecture.js +253 -90
- package/package.json +2 -2
- package/src/Asset.tsx +45 -17
- package/src/index.tsx +6 -1
- package/src/link.tsx +7 -7
package/dist/llms/rules/guide.js
CHANGED
|
@@ -52,6 +52,10 @@ export const Route = createFileRoute('/_authenticated')({
|
|
|
52
52
|
|
|
53
53
|
If your authentication check can throw errors (network failures, token validation, etc.), wrap it in try/catch:
|
|
54
54
|
|
|
55
|
+
<!-- ::start:framework -->
|
|
56
|
+
|
|
57
|
+
# React
|
|
58
|
+
|
|
55
59
|
\`\`\`tsx
|
|
56
60
|
import { createFileRoute, redirect, isRedirect } from '@tanstack/react-router'
|
|
57
61
|
|
|
@@ -81,6 +85,39 @@ export const Route = createFileRoute('/_authenticated')({
|
|
|
81
85
|
})
|
|
82
86
|
\`\`\`
|
|
83
87
|
|
|
88
|
+
# Solid
|
|
89
|
+
|
|
90
|
+
\`\`\`tsx
|
|
91
|
+
import { createFileRoute, redirect, isRedirect } from '@tanstack/solid-router'
|
|
92
|
+
|
|
93
|
+
// src/routes/_authenticated.tsx
|
|
94
|
+
export const Route = createFileRoute('/_authenticated')({
|
|
95
|
+
beforeLoad: async ({ location }) => {
|
|
96
|
+
try {
|
|
97
|
+
const user = await verifySession() // might throw on network error
|
|
98
|
+
if (!user) {
|
|
99
|
+
throw redirect({
|
|
100
|
+
to: '/login',
|
|
101
|
+
search: { redirect: location.href },
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
return { user }
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Re-throw redirects (they're intentional, not errors)
|
|
107
|
+
if (isRedirect(error)) throw error
|
|
108
|
+
|
|
109
|
+
// Auth check failed (network error, etc.) - redirect to login
|
|
110
|
+
throw redirect({
|
|
111
|
+
to: '/login',
|
|
112
|
+
search: { redirect: location.href },
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
\`\`\`
|
|
118
|
+
|
|
119
|
+
<!-- ::end:framework -->
|
|
120
|
+
|
|
84
121
|
The [\`isRedirect()\`](../api/router/isRedirectFunction.md) helper distinguishes between actual errors and intentional redirects.
|
|
85
122
|
|
|
86
123
|
Once you have authenticated a user, it's also common practice to redirect them back to the page they were trying to access. To do this, you can utilize the \`redirect\` search param that we added in our original redirect. Since we'll be replacing the entire URL with what it was, \`router.history.push\` is better suited for this than \`router.navigate\`:
|
|
@@ -119,9 +156,13 @@ We'll cover the \`router.context\` options in-detail in the [Router Context](./r
|
|
|
119
156
|
|
|
120
157
|
Here's an example that uses React context and hooks for protecting authenticated routes in TanStack Router. See the entire working setup in the [Authenticated Routes example](https://github.com/TanStack/router/tree/main/examples/react/authenticated-routes).
|
|
121
158
|
|
|
122
|
-
|
|
159
|
+
<!-- ::start:framework -->
|
|
123
160
|
|
|
124
|
-
|
|
161
|
+
# React
|
|
162
|
+
|
|
163
|
+
<!-- ::start:tabs variant="files" -->
|
|
164
|
+
|
|
165
|
+
\`\`\`tsx title="src/routes/__root.tsx"
|
|
125
166
|
import { createRootRouteWithContext } from '@tanstack/react-router'
|
|
126
167
|
|
|
127
168
|
interface MyRouterContext {
|
|
@@ -134,9 +175,7 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
|
|
|
134
175
|
})
|
|
135
176
|
\`\`\`
|
|
136
177
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
\`\`\`tsx
|
|
178
|
+
\`\`\`tsx title="src/router.tsx"
|
|
140
179
|
import { createRouter } from '@tanstack/react-router'
|
|
141
180
|
|
|
142
181
|
import { routeTree } from './routeTree.gen'
|
|
@@ -151,9 +190,7 @@ export const router = createRouter({
|
|
|
151
190
|
})
|
|
152
191
|
\`\`\`
|
|
153
192
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
\`\`\`tsx
|
|
193
|
+
\`\`\`tsx title="src/App.tsx"
|
|
157
194
|
import { RouterProvider } from '@tanstack/react-router'
|
|
158
195
|
|
|
159
196
|
import { AuthProvider, useAuth } from './auth'
|
|
@@ -174,11 +211,72 @@ function App() {
|
|
|
174
211
|
}
|
|
175
212
|
\`\`\`
|
|
176
213
|
|
|
214
|
+
<!-- ::end:tabs -->
|
|
215
|
+
|
|
216
|
+
# Solid
|
|
217
|
+
|
|
218
|
+
<!-- ::start:tabs variant="files" -->
|
|
219
|
+
|
|
220
|
+
\`\`\`tsx title="src/routes/__root.tsx"
|
|
221
|
+
import { createRootRouteWithContext } from '@tanstack/solid-router'
|
|
222
|
+
|
|
223
|
+
interface MyRouterContext {
|
|
224
|
+
// The ReturnType of your useAuth hook or the value of your AuthContext
|
|
225
|
+
auth: AuthState
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
|
229
|
+
component: () => <Outlet />,
|
|
230
|
+
})
|
|
231
|
+
\`\`\`
|
|
232
|
+
|
|
233
|
+
\`\`\`tsx title="src/router.tsx"
|
|
234
|
+
import { createRouter } from '@tanstack/solid-router'
|
|
235
|
+
|
|
236
|
+
import { routeTree } from './routeTree.gen'
|
|
237
|
+
|
|
238
|
+
export const router = createRouter({
|
|
239
|
+
routeTree,
|
|
240
|
+
context: {
|
|
241
|
+
// auth will initially be undefined
|
|
242
|
+
// We'll be passing down the auth state from within a React component
|
|
243
|
+
auth: undefined!,
|
|
244
|
+
},
|
|
245
|
+
})
|
|
246
|
+
\`\`\`
|
|
247
|
+
|
|
248
|
+
\`\`\`tsx title="src/App.tsx"
|
|
249
|
+
import { RouterProvider } from '@tanstack/solid-router'
|
|
250
|
+
|
|
251
|
+
import { AuthProvider, useAuth } from './auth'
|
|
252
|
+
|
|
253
|
+
import { router } from './router'
|
|
254
|
+
|
|
255
|
+
function InnerApp() {
|
|
256
|
+
const auth = useAuth()
|
|
257
|
+
return <RouterProvider router={router} context={{ auth }} />
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function App() {
|
|
261
|
+
return (
|
|
262
|
+
<AuthProvider>
|
|
263
|
+
<InnerApp />
|
|
264
|
+
</AuthProvider>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
\`\`\`
|
|
268
|
+
|
|
269
|
+
<!-- ::end:tabs -->
|
|
270
|
+
|
|
271
|
+
<!-- ::end:framework -->
|
|
272
|
+
|
|
177
273
|
Then in the authenticated route, you can check the auth state using the \`beforeLoad\` function, and **throw a \`redirect()\`** to your **Login route** if the user is not signed-in.
|
|
178
274
|
|
|
179
|
-
|
|
275
|
+
<!-- ::start:framework -->
|
|
180
276
|
|
|
181
|
-
|
|
277
|
+
# React
|
|
278
|
+
|
|
279
|
+
\`\`\`tsx title="src/routes/dashboard.route.tsx"
|
|
182
280
|
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
183
281
|
|
|
184
282
|
export const Route = createFileRoute('/dashboard')({
|
|
@@ -195,6 +293,27 @@ export const Route = createFileRoute('/dashboard')({
|
|
|
195
293
|
})
|
|
196
294
|
\`\`\`
|
|
197
295
|
|
|
296
|
+
# Solid
|
|
297
|
+
|
|
298
|
+
\`\`\`tsx title="src/routes/dashboard.route.tsx"
|
|
299
|
+
import { createFileRoute, redirect } from '@tanstack/solid-router'
|
|
300
|
+
|
|
301
|
+
export const Route = createFileRoute('/dashboard')({
|
|
302
|
+
beforeLoad: ({ context, location }) => {
|
|
303
|
+
if (!context.auth.isAuthenticated()) {
|
|
304
|
+
throw redirect({
|
|
305
|
+
to: '/login',
|
|
306
|
+
search: {
|
|
307
|
+
redirect: location.href,
|
|
308
|
+
},
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
\`\`\`
|
|
314
|
+
|
|
315
|
+
<!-- ::end:framework -->
|
|
316
|
+
|
|
198
317
|
You can _optionally_, also use the [Non-Redirected Authentication](#non-redirected-authentication) approach to show a login form instead of calling a **redirect**.
|
|
199
318
|
|
|
200
319
|
This approach can also be used in conjunction with Pathless or Layout Route to protect all routes under their parent route.
|
|
@@ -298,8 +417,6 @@ For automatic code splitting to work, there are some rules in-place to make sure
|
|
|
298
417
|
Route properties like \`component\`, \`loader\`, etc., should not be exported from the route file. Exporting these properties results in them being bundled into the main application bundle, which means that they will not be code-split.
|
|
299
418
|
|
|
300
419
|
\`\`\`tsx
|
|
301
|
-
import { createRoute } from '@tanstack/react-router'
|
|
302
|
-
|
|
303
420
|
export const Route = createRoute('/posts')({
|
|
304
421
|
// ...
|
|
305
422
|
notFoundComponent: PostsNotFoundComponent,
|
|
@@ -331,8 +448,7 @@ You can change how TanStack Router splits your routes by changing the \`defaultB
|
|
|
331
448
|
|
|
332
449
|
For example, to bundle all UI-related components into a single chunk, you could configure it like this:
|
|
333
450
|
|
|
334
|
-
\`\`\`ts
|
|
335
|
-
// vite.config.ts
|
|
451
|
+
\`\`\`ts title="vite.config.ts"
|
|
336
452
|
import { defineConfig } from 'vite'
|
|
337
453
|
import { tanstackRouter } from '@tanstack/router-plugin/vite'
|
|
338
454
|
|
|
@@ -359,8 +475,7 @@ export default defineConfig({
|
|
|
359
475
|
|
|
360
476
|
For complex rulesets, you can use the \`splitBehavior\` function in your vite config to programmatically define how routes should be split into chunks based on their \`routeId\`. This function allows you to implement custom logic for grouping properties together, giving you fine-grained control over the code splitting behavior.
|
|
361
477
|
|
|
362
|
-
\`\`\`ts
|
|
363
|
-
// vite.config.ts
|
|
478
|
+
\`\`\`ts title="vite.config.ts"
|
|
364
479
|
import { defineConfig } from 'vite'
|
|
365
480
|
import { tanstackRouter } from '@tanstack/router-plugin/vite'
|
|
366
481
|
|
|
@@ -386,9 +501,7 @@ export default defineConfig({
|
|
|
386
501
|
|
|
387
502
|
For ultimate control, you can override the global configuration directly inside a route file by adding a \`codeSplitGroupings\` property. This is useful for routes that have unique optimization needs.
|
|
388
503
|
|
|
389
|
-
\`\`\`tsx
|
|
390
|
-
// src/routes/posts.route.tsx
|
|
391
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
504
|
+
\`\`\`tsx title="src/routes/posts.route.tsx"
|
|
392
505
|
import { loadPostsData } from './-heavy-posts-utils'
|
|
393
506
|
|
|
394
507
|
export const Route = createFileRoute('/posts')({
|
|
@@ -565,8 +678,11 @@ When you are using \`.lazy.tsx\` you can split your route into two files to enab
|
|
|
565
678
|
|
|
566
679
|
**Before (Single File)**
|
|
567
680
|
|
|
568
|
-
|
|
569
|
-
|
|
681
|
+
<!-- ::start:framework -->
|
|
682
|
+
|
|
683
|
+
# React
|
|
684
|
+
|
|
685
|
+
\`\`\`tsx title="src/routes/posts.tsx"
|
|
570
686
|
import { createFileRoute } from '@tanstack/react-router'
|
|
571
687
|
import { fetchPosts } from './api'
|
|
572
688
|
|
|
@@ -580,13 +696,33 @@ function Posts() {
|
|
|
580
696
|
}
|
|
581
697
|
\`\`\`
|
|
582
698
|
|
|
699
|
+
# Solid
|
|
700
|
+
|
|
701
|
+
\`\`\`tsx title="src/routes/posts.tsx"
|
|
702
|
+
import { createFileRoute } from '@tanstack/solid-router'
|
|
703
|
+
import { fetchPosts } from './api'
|
|
704
|
+
|
|
705
|
+
export const Route = createFileRoute('/posts')({
|
|
706
|
+
loader: fetchPosts,
|
|
707
|
+
component: Posts,
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
function Posts() {
|
|
711
|
+
// ...
|
|
712
|
+
}
|
|
713
|
+
\`\`\`
|
|
714
|
+
|
|
715
|
+
<!-- ::end:framework -->
|
|
716
|
+
|
|
583
717
|
**After (Split into two files)**
|
|
584
718
|
|
|
585
719
|
This file would contain the critical route configuration:
|
|
586
720
|
|
|
587
|
-
|
|
588
|
-
|
|
721
|
+
<!-- ::start:framework -->
|
|
722
|
+
|
|
723
|
+
# React
|
|
589
724
|
|
|
725
|
+
\`\`\`tsx title="src/routes/posts.tsx"
|
|
590
726
|
import { createFileRoute } from '@tanstack/react-router'
|
|
591
727
|
import { fetchPosts } from './api'
|
|
592
728
|
|
|
@@ -595,10 +731,26 @@ export const Route = createFileRoute('/posts')({
|
|
|
595
731
|
})
|
|
596
732
|
\`\`\`
|
|
597
733
|
|
|
734
|
+
# Solid
|
|
735
|
+
|
|
736
|
+
\`\`\`tsx title="src/routes/posts.tsx"
|
|
737
|
+
import { createFileRoute } from '@tanstack/solid-router'
|
|
738
|
+
import { fetchPosts } from './api'
|
|
739
|
+
|
|
740
|
+
export const Route = createFileRoute('/posts')({
|
|
741
|
+
loader: fetchPosts,
|
|
742
|
+
})
|
|
743
|
+
\`\`\`
|
|
744
|
+
|
|
745
|
+
<!-- ::end:framework -->
|
|
746
|
+
|
|
598
747
|
With the non-critical route configuration going into the file with the \`.lazy.tsx\` suffix:
|
|
599
748
|
|
|
600
|
-
|
|
601
|
-
|
|
749
|
+
<!-- ::start:framework -->
|
|
750
|
+
|
|
751
|
+
# React
|
|
752
|
+
|
|
753
|
+
\`\`\`tsx title="src/routes/posts.lazy.tsx"
|
|
602
754
|
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
603
755
|
|
|
604
756
|
export const Route = createLazyFileRoute('/posts')({
|
|
@@ -610,14 +762,35 @@ function Posts() {
|
|
|
610
762
|
}
|
|
611
763
|
\`\`\`
|
|
612
764
|
|
|
765
|
+
# Solid
|
|
766
|
+
|
|
767
|
+
\`\`\`tsx title="src/routes/posts.lazy.tsx"
|
|
768
|
+
import { createLazyFileRoute } from '@tanstack/solid-router'
|
|
769
|
+
|
|
770
|
+
export const Route = createLazyFileRoute('/posts')({
|
|
771
|
+
component: Posts,
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
function Posts() {
|
|
775
|
+
// ...
|
|
776
|
+
}
|
|
777
|
+
\`\`\`
|
|
778
|
+
|
|
779
|
+
<!-- ::end:framework -->
|
|
780
|
+
|
|
613
781
|
## Using Virtual Routes
|
|
614
782
|
|
|
615
783
|
You might run into a situation where you end up splitting out everything from a route file, leaving it empty! In this case, simply **delete the route file entirely**! A virtual route will automatically be generated for you to serve as an anchor for your code split files. This virtual route will live directly in the generated route tree file.
|
|
616
784
|
|
|
617
785
|
**Before (Virtual Routes)**
|
|
618
786
|
|
|
619
|
-
|
|
620
|
-
|
|
787
|
+
<!-- ::start:framework -->
|
|
788
|
+
|
|
789
|
+
# React
|
|
790
|
+
|
|
791
|
+
<!-- ::start:tabs variant="files" -->
|
|
792
|
+
|
|
793
|
+
\`\`\`tsx title="src/routes/posts.tsx"
|
|
621
794
|
import { createFileRoute } from '@tanstack/react-router'
|
|
622
795
|
|
|
623
796
|
export const Route = createFileRoute('/posts')({
|
|
@@ -625,8 +798,7 @@ export const Route = createFileRoute('/posts')({
|
|
|
625
798
|
})
|
|
626
799
|
\`\`\`
|
|
627
800
|
|
|
628
|
-
\`\`\`tsx
|
|
629
|
-
// src/routes/posts.lazy.tsx
|
|
801
|
+
\`\`\`tsx title="src/routes/posts.lazy.tsx"
|
|
630
802
|
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
631
803
|
|
|
632
804
|
export const Route = createLazyFileRoute('/posts')({
|
|
@@ -638,10 +810,43 @@ function Posts() {
|
|
|
638
810
|
}
|
|
639
811
|
\`\`\`
|
|
640
812
|
|
|
813
|
+
<!-- ::end:tabs -->
|
|
814
|
+
|
|
815
|
+
# Solid
|
|
816
|
+
|
|
817
|
+
<!-- ::start:tabs variant="files" -->
|
|
818
|
+
|
|
819
|
+
\`\`\`tsx title="src/routes/posts.tsx"
|
|
820
|
+
import { createFileRoute } from '@tanstack/solid-router'
|
|
821
|
+
|
|
822
|
+
export const Route = createFileRoute('/posts')({
|
|
823
|
+
// Hello?
|
|
824
|
+
})
|
|
825
|
+
\`\`\`
|
|
826
|
+
|
|
827
|
+
\`\`\`tsx title="src/routes/posts.lazy.tsx"
|
|
828
|
+
import { createLazyFileRoute } from '@tanstack/solid-router'
|
|
829
|
+
|
|
830
|
+
export const Route = createLazyFileRoute('/posts')({
|
|
831
|
+
component: Posts,
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
function Posts() {
|
|
835
|
+
// ...
|
|
836
|
+
}
|
|
837
|
+
\`\`\`
|
|
838
|
+
|
|
839
|
+
<!-- ::end:tabs -->
|
|
840
|
+
|
|
841
|
+
<!-- ::end:framework -->
|
|
842
|
+
|
|
641
843
|
**After (Virtual Routes)**
|
|
642
844
|
|
|
643
|
-
|
|
644
|
-
|
|
845
|
+
<!-- ::start:framework -->
|
|
846
|
+
|
|
847
|
+
# React
|
|
848
|
+
|
|
849
|
+
\`\`\`tsx title="src/routes/posts.lazy.tsx"
|
|
645
850
|
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
646
851
|
|
|
647
852
|
export const Route = createLazyFileRoute('/posts')({
|
|
@@ -653,6 +858,22 @@ function Posts() {
|
|
|
653
858
|
}
|
|
654
859
|
\`\`\`
|
|
655
860
|
|
|
861
|
+
# Solid
|
|
862
|
+
|
|
863
|
+
\`\`\`tsx title="src/routes/posts.lazy.tsx"
|
|
864
|
+
import { createLazyFileRoute } from '@tanstack/solid-router'
|
|
865
|
+
|
|
866
|
+
export const Route = createLazyFileRoute('/posts')({
|
|
867
|
+
component: Posts,
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
function Posts() {
|
|
871
|
+
// ...
|
|
872
|
+
}
|
|
873
|
+
\`\`\`
|
|
874
|
+
|
|
875
|
+
<!-- ::end:framework -->
|
|
876
|
+
|
|
656
877
|
Tada! 🎉
|
|
657
878
|
|
|
658
879
|
## Code-Based Splitting
|
|
@@ -661,8 +882,7 @@ If you are using code-based routing, you can still code-split your routes using
|
|
|
661
882
|
|
|
662
883
|
Create a lazy route using the \`createLazyRoute\` function.
|
|
663
884
|
|
|
664
|
-
\`\`\`tsx
|
|
665
|
-
// src/posts.lazy.tsx
|
|
885
|
+
\`\`\`tsx title="src/posts.lazy.tsx"
|
|
666
886
|
export const Route = createLazyRoute('/posts')({
|
|
667
887
|
component: MyComponent,
|
|
668
888
|
})
|
|
@@ -674,8 +894,7 @@ function MyComponent() {
|
|
|
674
894
|
|
|
675
895
|
Then, call the \`.lazy\` method on the route definition in your \`app.tsx\` to import the lazy/code-split route with the non-critical route configuration.
|
|
676
896
|
|
|
677
|
-
\`\`\`tsx
|
|
678
|
-
// src/app.tsx
|
|
897
|
+
\`\`\`tsx title="src/app.tsx"
|
|
679
898
|
const postsRoute = createRoute({
|
|
680
899
|
getParentRoute: () => rootRoute,
|
|
681
900
|
path: '/posts',
|
|
@@ -690,6 +909,10 @@ It can be a powerful tool to reduce bundle size, but it comes with a cost as men
|
|
|
690
909
|
|
|
691
910
|
You can code split your data loading logic using the Route's \`loader\` option. While this process makes it difficult to maintain type-safety with the parameters passed to your loader, you can always use the generic \`LoaderContext\` type to get you most of the way there:
|
|
692
911
|
|
|
912
|
+
<!-- ::start:framework -->
|
|
913
|
+
|
|
914
|
+
# React
|
|
915
|
+
|
|
693
916
|
\`\`\`tsx
|
|
694
917
|
import { lazyFn } from '@tanstack/react-router'
|
|
695
918
|
|
|
@@ -705,15 +928,38 @@ export const loader = async (context: LoaderContext) => {
|
|
|
705
928
|
}
|
|
706
929
|
\`\`\`
|
|
707
930
|
|
|
931
|
+
# Solid
|
|
932
|
+
|
|
933
|
+
\`\`\`tsx
|
|
934
|
+
import { lazyFn } from '@tanstack/solid-router'
|
|
935
|
+
|
|
936
|
+
const route = createRoute({
|
|
937
|
+
path: '/my-route',
|
|
938
|
+
component: MyComponent,
|
|
939
|
+
loader: lazyFn(() => import('./loader'), 'loader'),
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
// In another file...a
|
|
943
|
+
export const loader = async (context: LoaderContext) => {
|
|
944
|
+
/// ...
|
|
945
|
+
}
|
|
946
|
+
\`\`\`
|
|
947
|
+
|
|
948
|
+
<!-- ::end:framework -->
|
|
949
|
+
|
|
708
950
|
If you are using file-based routing, you'll only be able to split your \`loader\` if you are using [Automatic Code Splitting](#using-automatic-code-splitting) with customized bundling options.
|
|
709
951
|
|
|
710
952
|
## Manually accessing Route APIs in other files with the \`getRouteApi\` helper
|
|
711
953
|
|
|
712
954
|
As you might have guessed, placing your component code in a separate file than your route can make it difficult to consume the route itself. To help with this, TanStack Router exports a handy \`getRouteApi\` function that you can use to access a route's type-safe APIs in a file without importing the route itself.
|
|
713
955
|
|
|
714
|
-
|
|
956
|
+
<!-- ::start:framework -->
|
|
715
957
|
|
|
716
|
-
|
|
958
|
+
# React
|
|
959
|
+
|
|
960
|
+
<!-- ::start:tabs variant="files" -->
|
|
961
|
+
|
|
962
|
+
\`\`\`tsx title="src/my-route.tsx"
|
|
717
963
|
import { createRoute } from '@tanstack/react-router'
|
|
718
964
|
import { MyComponent } from './MyComponent'
|
|
719
965
|
|
|
@@ -726,9 +972,7 @@ const route = createRoute({
|
|
|
726
972
|
})
|
|
727
973
|
\`\`\`
|
|
728
974
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
\`\`\`tsx
|
|
975
|
+
\`\`\`tsx title="src/MyComponent.tsx"
|
|
732
976
|
import { getRouteApi } from '@tanstack/react-router'
|
|
733
977
|
|
|
734
978
|
const route = getRouteApi('/my-route')
|
|
@@ -741,6 +985,42 @@ export function MyComponent() {
|
|
|
741
985
|
}
|
|
742
986
|
\`\`\`
|
|
743
987
|
|
|
988
|
+
<!-- ::end:tabs -->
|
|
989
|
+
|
|
990
|
+
# Solid
|
|
991
|
+
|
|
992
|
+
<!-- ::start:tabs variant="files" -->
|
|
993
|
+
|
|
994
|
+
\`\`\`tsx title="src/my-route.tsx"
|
|
995
|
+
import { createRoute } from '@tanstack/solid-router'
|
|
996
|
+
import { MyComponent } from './MyComponent'
|
|
997
|
+
|
|
998
|
+
const route = createRoute({
|
|
999
|
+
path: '/my-route',
|
|
1000
|
+
loader: () => ({
|
|
1001
|
+
foo: 'bar',
|
|
1002
|
+
}),
|
|
1003
|
+
component: MyComponent,
|
|
1004
|
+
})
|
|
1005
|
+
\`\`\`
|
|
1006
|
+
|
|
1007
|
+
\`\`\`tsx title="src/MyComponent.tsx"
|
|
1008
|
+
import { getRouteApi } from '@tanstack/solid-router'
|
|
1009
|
+
|
|
1010
|
+
const route = getRouteApi('/my-route')
|
|
1011
|
+
|
|
1012
|
+
export function MyComponent() {
|
|
1013
|
+
const loaderData = route.useLoaderData()
|
|
1014
|
+
// ^? { foo: string }
|
|
1015
|
+
|
|
1016
|
+
return <div>...</div>
|
|
1017
|
+
}
|
|
1018
|
+
\`\`\`
|
|
1019
|
+
|
|
1020
|
+
<!-- ::end:tabs -->
|
|
1021
|
+
|
|
1022
|
+
<!-- ::end:framework -->
|
|
1023
|
+
|
|
744
1024
|
The \`getRouteApi\` function is useful for accessing other type-safe APIs:
|
|
745
1025
|
|
|
746
1026
|
- \`useLoaderData\`
|
|
@@ -752,11 +1032,15 @@ The \`getRouteApi\` function is useful for accessing other type-safe APIs:
|
|
|
752
1032
|
|
|
753
1033
|
# Creating a Router
|
|
754
1034
|
|
|
755
|
-
## The \`
|
|
1035
|
+
## The \`createRouter\` function
|
|
756
1036
|
|
|
757
1037
|
When you're ready to start using your router, you'll need to create a new \`Router\` instance. The router instance is the core brains of TanStack Router and is responsible for managing the route tree, matching routes, and coordinating navigations and route transitions. It also serves as a place to configure router-wide settings.
|
|
758
1038
|
|
|
759
|
-
|
|
1039
|
+
<!-- ::start:framework -->
|
|
1040
|
+
|
|
1041
|
+
# React
|
|
1042
|
+
|
|
1043
|
+
\`\`\`tsx title="src/router.tsx"
|
|
760
1044
|
import { createRouter } from '@tanstack/react-router'
|
|
761
1045
|
|
|
762
1046
|
const router = createRouter({
|
|
@@ -764,7 +1048,19 @@ const router = createRouter({
|
|
|
764
1048
|
})
|
|
765
1049
|
\`\`\`
|
|
766
1050
|
|
|
767
|
-
|
|
1051
|
+
# Solid
|
|
1052
|
+
|
|
1053
|
+
\`\`\`tsx title="src/router.tsx"
|
|
1054
|
+
import { createRouter } from '@tanstack/solid-router'
|
|
1055
|
+
|
|
1056
|
+
const router = createRouter({
|
|
1057
|
+
// ...
|
|
1058
|
+
})
|
|
1059
|
+
\`\`\`
|
|
1060
|
+
|
|
1061
|
+
<!-- ::end:framework -->
|
|
1062
|
+
|
|
1063
|
+
## Route Tree
|
|
768
1064
|
|
|
769
1065
|
You'll probably notice quickly that the \`Router\` constructor requires a \`routeTree\` option. This is the route tree that the router will use to match routes and render components.
|
|
770
1066
|
|
|
@@ -795,7 +1091,11 @@ const routeTree = rootRoute.addChildren([
|
|
|
795
1091
|
|
|
796
1092
|
TanStack Router provides amazing support for TypeScript, even for things you wouldn't expect like bare imports straight from the library! To make this possible, you must register your router's types using TypeScripts' [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) feature. This is done by extending the \`Register\` interface on \`@tanstack/react-router\` with a \`router\` property that has the type of your \`router\` instance:
|
|
797
1093
|
|
|
798
|
-
|
|
1094
|
+
<!-- ::start:framework -->
|
|
1095
|
+
|
|
1096
|
+
# React
|
|
1097
|
+
|
|
1098
|
+
\`\`\`tsx title="src/router.tsx"
|
|
799
1099
|
declare module '@tanstack/react-router' {
|
|
800
1100
|
interface Register {
|
|
801
1101
|
// This infers the type of our router and registers it across your entire project
|
|
@@ -804,6 +1104,19 @@ declare module '@tanstack/react-router' {
|
|
|
804
1104
|
}
|
|
805
1105
|
\`\`\`
|
|
806
1106
|
|
|
1107
|
+
# Solid
|
|
1108
|
+
|
|
1109
|
+
\`\`\`tsx title="src/router.tsx"
|
|
1110
|
+
declare module '@tanstack/solid-router' {
|
|
1111
|
+
interface Register {
|
|
1112
|
+
// This infers the type of our router and registers it across your entire project
|
|
1113
|
+
router: typeof router
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
\`\`\`
|
|
1117
|
+
|
|
1118
|
+
<!-- ::end:framework -->
|
|
1119
|
+
|
|
807
1120
|
With your router registered, you'll now get type-safety across your entire project for anything related to routing.
|
|
808
1121
|
|
|
809
1122
|
## 404 Not Found Route
|
|
@@ -837,7 +1150,9 @@ While repeating yourself can be acceptable in many situations, you might find th
|
|
|
837
1150
|
|
|
838
1151
|
If you want to create a basic custom link component, you can do so with the following:
|
|
839
1152
|
|
|
840
|
-
|
|
1153
|
+
<!-- ::start:framework -->
|
|
1154
|
+
|
|
1155
|
+
# React
|
|
841
1156
|
|
|
842
1157
|
\`\`\`tsx
|
|
843
1158
|
import * as React from 'react'
|
|
@@ -862,7 +1177,34 @@ export const CustomLink: LinkComponent<typeof BasicLinkComponent> = (props) => {
|
|
|
862
1177
|
}
|
|
863
1178
|
\`\`\`
|
|
864
1179
|
|
|
865
|
-
|
|
1180
|
+
# Solid
|
|
1181
|
+
|
|
1182
|
+
\`\`\`tsx
|
|
1183
|
+
import * as Solid from 'solid-js'
|
|
1184
|
+
import { createLink, LinkComponent } from '@tanstack/solid-router'
|
|
1185
|
+
|
|
1186
|
+
export const Route = createRootRoute({
|
|
1187
|
+
component: RootComponent,
|
|
1188
|
+
})
|
|
1189
|
+
|
|
1190
|
+
type BasicLinkProps = Solid.JSX.IntrinsicElements['a'] & {
|
|
1191
|
+
// Add any additional props you want to pass to the anchor element
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const BasicLinkComponent: Solid.Component<BasicLinkProps> = (props) => (
|
|
1195
|
+
<a {...props} class="block px-3 py-2 text-red-700">
|
|
1196
|
+
{props.children}
|
|
1197
|
+
</a>
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
const CreatedLinkComponent = createLink(BasicLinkComponent)
|
|
1201
|
+
|
|
1202
|
+
export const CustomLink: LinkComponent<typeof BasicLinkComponent> = (props) => {
|
|
1203
|
+
return <CreatedLinkComponent preload={'intent'} {...props} />
|
|
1204
|
+
}
|
|
1205
|
+
\`\`\`
|
|
1206
|
+
|
|
1207
|
+
<!-- ::end:framework -->
|
|
866
1208
|
|
|
867
1209
|
You can then use your newly created \`Link\` component as any other \`Link\`
|
|
868
1210
|
|
|
@@ -870,17 +1212,21 @@ You can then use your newly created \`Link\` component as any other \`Link\`
|
|
|
870
1212
|
<CustomLink to={'/dashboard/invoices/$invoiceId'} params={{ invoiceId: 0 }} />
|
|
871
1213
|
\`\`\`
|
|
872
1214
|
|
|
873
|
-
[//]: # 'ExamplesUsingThirdPartyLibs'
|
|
874
|
-
|
|
875
1215
|
## \`createLink\` with third party libraries
|
|
876
1216
|
|
|
877
1217
|
Here are some examples of how you can use \`createLink\` with third-party libraries.
|
|
878
1218
|
|
|
1219
|
+
<!-- ::start:framework -->
|
|
1220
|
+
|
|
1221
|
+
# React
|
|
1222
|
+
|
|
879
1223
|
### React Aria Components example
|
|
880
1224
|
|
|
881
1225
|
React Aria Components v1.11.0 and later works with TanStack Router's \`preload (intent)\` prop. Use \`createLink\` to wrap each React Aria component that you use as a link.
|
|
882
1226
|
|
|
883
|
-
|
|
1227
|
+
<!-- ::start:tabs variant="files" -->
|
|
1228
|
+
|
|
1229
|
+
\`\`\`tsx title="RACLink.tsx"
|
|
884
1230
|
import { createLink } from '@tanstack/react-router'
|
|
885
1231
|
import { Link as RACLink, MenuItem } from 'react-aria-components'
|
|
886
1232
|
|
|
@@ -888,9 +1234,7 @@ export const Link = createLink(RACLink)
|
|
|
888
1234
|
export const MenuItemLink = createLink(MenuItem)
|
|
889
1235
|
\`\`\`
|
|
890
1236
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
\`\`\`tsx
|
|
1237
|
+
\`\`\`tsx title="CustomRACLink.tsx"
|
|
894
1238
|
import { createLink } from '@tanstack/react-router'
|
|
895
1239
|
import { Link as RACLink, type LinkProps } from 'react-aria-components'
|
|
896
1240
|
|
|
@@ -912,9 +1256,19 @@ function MyLink(props: MyLinkProps) {
|
|
|
912
1256
|
export const Link = createLink(MyLink)
|
|
913
1257
|
\`\`\`
|
|
914
1258
|
|
|
1259
|
+
<!-- ::end:tabs -->
|
|
1260
|
+
|
|
1261
|
+
To use React Aria's render props, including the \`className\`, \`style\`, and \`children\` functions, create a wrapper component and pass that to \`createLink\`.
|
|
1262
|
+
|
|
1263
|
+
<!-- ::end:framework -->
|
|
1264
|
+
|
|
1265
|
+
<!-- ::start:framework -->
|
|
1266
|
+
|
|
1267
|
+
# React
|
|
1268
|
+
|
|
915
1269
|
### Chakra UI example
|
|
916
1270
|
|
|
917
|
-
\`\`\`tsx
|
|
1271
|
+
\`\`\`tsx title="ChakraLinkComponent.tsx"
|
|
918
1272
|
import * as React from 'react'
|
|
919
1273
|
import { createLink, LinkComponent } from '@tanstack/react-router'
|
|
920
1274
|
import { Link } from '@chakra-ui/react'
|
|
@@ -950,6 +1304,12 @@ export const CustomLink: LinkComponent<typeof ChakraLinkComponent> = (
|
|
|
950
1304
|
}
|
|
951
1305
|
\`\`\`
|
|
952
1306
|
|
|
1307
|
+
<!-- ::end:framework -->
|
|
1308
|
+
|
|
1309
|
+
<!-- ::start:framework -->
|
|
1310
|
+
|
|
1311
|
+
# React
|
|
1312
|
+
|
|
953
1313
|
### MUI example
|
|
954
1314
|
|
|
955
1315
|
There is an [example](https://github.com/TanStack/router/tree/main/examples/react/start-material-ui) available which uses these patterns.
|
|
@@ -958,16 +1318,22 @@ There is an [example](https://github.com/TanStack/router/tree/main/examples/reac
|
|
|
958
1318
|
|
|
959
1319
|
If the MUI \`Link\` should simply behave like the router \`Link\`, it can be just wrapped with \`createLink\`:
|
|
960
1320
|
|
|
961
|
-
|
|
1321
|
+
<!-- ::start:tabs variant="files" -->
|
|
1322
|
+
|
|
1323
|
+
\`\`\`tsx title="CustomLink.tsx"
|
|
962
1324
|
import { createLink } from '@tanstack/react-router'
|
|
963
1325
|
import { Link } from '@mui/material'
|
|
964
1326
|
|
|
965
1327
|
export const CustomLink = createLink(Link)
|
|
966
1328
|
\`\`\`
|
|
967
1329
|
|
|
1330
|
+
<!-- ::end:tabs -->
|
|
1331
|
+
|
|
968
1332
|
If the \`Link\` should be customized this approach can be used:
|
|
969
1333
|
|
|
970
|
-
|
|
1334
|
+
<!-- ::start:tabs variant="files" -->
|
|
1335
|
+
|
|
1336
|
+
\`\`\`tsx title="CustomLink.tsx"
|
|
971
1337
|
import React from 'react'
|
|
972
1338
|
import { createLink } from '@tanstack/react-router'
|
|
973
1339
|
import { Link } from '@mui/material'
|
|
@@ -991,11 +1357,15 @@ export const CustomLink: LinkComponent<typeof MUILinkComponent> = (props) => {
|
|
|
991
1357
|
// Can also be styled
|
|
992
1358
|
\`\`\`
|
|
993
1359
|
|
|
1360
|
+
<!-- ::end:tabs -->
|
|
1361
|
+
|
|
994
1362
|
#### \`Button\`
|
|
995
1363
|
|
|
996
1364
|
If a \`Button\` should be used as a router \`Link\`, the \`component\` should be set as \`a\`:
|
|
997
1365
|
|
|
998
|
-
|
|
1366
|
+
<!-- ::start:tabs variant="files" -->
|
|
1367
|
+
|
|
1368
|
+
\`\`\`tsx title="CustomButtonLink.tsx"
|
|
999
1369
|
import React from 'react'
|
|
1000
1370
|
import { createLink } from '@tanstack/react-router'
|
|
1001
1371
|
import { Button } from '@mui/material'
|
|
@@ -1020,11 +1390,15 @@ export const CustomButtonLink: LinkComponent<typeof MUIButtonLinkComponent> = (
|
|
|
1020
1390
|
}
|
|
1021
1391
|
\`\`\`
|
|
1022
1392
|
|
|
1393
|
+
<!-- ::end:tabs -->
|
|
1394
|
+
|
|
1023
1395
|
#### Usage with \`styled\`
|
|
1024
1396
|
|
|
1025
1397
|
Any of these MUI approaches can then be used with \`styled\`:
|
|
1026
1398
|
|
|
1027
|
-
|
|
1399
|
+
<!-- ::start:tabs variant="files" -->
|
|
1400
|
+
|
|
1401
|
+
\`\`\`tsx title="StyledCustomLink.tsx"
|
|
1028
1402
|
import { css, styled } from '@mui/material'
|
|
1029
1403
|
import { CustomLink } from './CustomLink'
|
|
1030
1404
|
|
|
@@ -1035,9 +1409,19 @@ const StyledCustomLink = styled(CustomLink)(
|
|
|
1035
1409
|
)
|
|
1036
1410
|
\`\`\`
|
|
1037
1411
|
|
|
1412
|
+
<!-- ::end:tabs -->
|
|
1413
|
+
|
|
1414
|
+
<!-- ::end:framework -->
|
|
1415
|
+
|
|
1416
|
+
<!-- ::start:framework -->
|
|
1417
|
+
|
|
1418
|
+
# React
|
|
1419
|
+
|
|
1038
1420
|
### Mantine example
|
|
1039
1421
|
|
|
1040
|
-
|
|
1422
|
+
<!-- ::start:tabs variant="files" -->
|
|
1423
|
+
|
|
1424
|
+
\`\`\`tsx title="CustomLink.tsx"
|
|
1041
1425
|
import * as React from 'react'
|
|
1042
1426
|
import { createLink, LinkComponent } from '@tanstack/react-router'
|
|
1043
1427
|
import { Anchor, AnchorProps } from '@mantine/core'
|
|
@@ -1062,7 +1446,25 @@ export const CustomLink: LinkComponent<typeof MantineLinkComponent> = (
|
|
|
1062
1446
|
}
|
|
1063
1447
|
\`\`\`
|
|
1064
1448
|
|
|
1065
|
-
|
|
1449
|
+
<!-- ::end:tabs -->
|
|
1450
|
+
|
|
1451
|
+
<!-- ::end:framework -->
|
|
1452
|
+
|
|
1453
|
+
<!-- ::start:framework -->
|
|
1454
|
+
|
|
1455
|
+
# Solid
|
|
1456
|
+
|
|
1457
|
+
### Some Library example
|
|
1458
|
+
|
|
1459
|
+
<!-- ::start:tabs variant="files" -->
|
|
1460
|
+
|
|
1461
|
+
\`\`\`tsx title="UntitledLink.tsx"
|
|
1462
|
+
// TODO: Add this example.
|
|
1463
|
+
\`\`\`
|
|
1464
|
+
|
|
1465
|
+
<!-- ::end:tabs -->
|
|
1466
|
+
|
|
1467
|
+
<!-- ::end:framework -->
|
|
1066
1468
|
|
|
1067
1469
|
# Custom Search Param Serialization
|
|
1068
1470
|
|
|
@@ -1086,6 +1488,10 @@ It would be serialized and escaped into the following search string:
|
|
|
1086
1488
|
|
|
1087
1489
|
We can implement the default behavior with the following code:
|
|
1088
1490
|
|
|
1491
|
+
<!-- ::start:framework -->
|
|
1492
|
+
|
|
1493
|
+
# React
|
|
1494
|
+
|
|
1089
1495
|
\`\`\`tsx
|
|
1090
1496
|
import {
|
|
1091
1497
|
createRouter,
|
|
@@ -1100,6 +1506,24 @@ const router = createRouter({
|
|
|
1100
1506
|
})
|
|
1101
1507
|
\`\`\`
|
|
1102
1508
|
|
|
1509
|
+
# Solid
|
|
1510
|
+
|
|
1511
|
+
\`\`\`tsx
|
|
1512
|
+
import {
|
|
1513
|
+
createRouter,
|
|
1514
|
+
parseSearchWith,
|
|
1515
|
+
stringifySearchWith,
|
|
1516
|
+
} from '@tanstack/solid-router'
|
|
1517
|
+
|
|
1518
|
+
const router = createRouter({
|
|
1519
|
+
// ...
|
|
1520
|
+
parseSearch: parseSearchWith(JSON.parse),
|
|
1521
|
+
stringifySearch: stringifySearchWith(JSON.stringify),
|
|
1522
|
+
})
|
|
1523
|
+
\`\`\`
|
|
1524
|
+
|
|
1525
|
+
<!-- ::end:framework -->
|
|
1526
|
+
|
|
1103
1527
|
However, this default behavior may not be suitable for all use cases. For example, you may want to use a different serialization format, such as base64 encoding, or you may want to use a purpose-built serialization/deserialization library, like [query-string](https://github.com/sindresorhus/query-string), [JSURL2](https://github.com/wmertens/jsurl2), or [Zipson](https://jgranstrom.github.io/zipson/).
|
|
1104
1528
|
|
|
1105
1529
|
This can be achieved by providing your own serialization and deserialization functions to the \`parseSearch\` and \`stringifySearch\` options in the [\`Router\`](../api/router/RouterOptionsType.md#stringifysearch-method) configuration. When doing this, you can utilize TanStack Router's built-in helper functions, \`parseSearchWith\` and \`stringifySearchWith\`, to simplify the process.
|
|
@@ -1115,6 +1539,10 @@ Here are some examples of how you can customize the search param serialization i
|
|
|
1115
1539
|
|
|
1116
1540
|
It's common to base64 encode your search params to achieve maximum compatibility across browsers and URL unfurlers, etc. This can be done with the following code:
|
|
1117
1541
|
|
|
1542
|
+
<!-- ::start:framework -->
|
|
1543
|
+
|
|
1544
|
+
# React
|
|
1545
|
+
|
|
1118
1546
|
\`\`\`tsx
|
|
1119
1547
|
import {
|
|
1120
1548
|
Router,
|
|
@@ -1148,6 +1576,43 @@ function encodeToBinary(str: string): string {
|
|
|
1148
1576
|
}
|
|
1149
1577
|
\`\`\`
|
|
1150
1578
|
|
|
1579
|
+
# Solid
|
|
1580
|
+
|
|
1581
|
+
\`\`\`tsx
|
|
1582
|
+
import {
|
|
1583
|
+
Router,
|
|
1584
|
+
parseSearchWith,
|
|
1585
|
+
stringifySearchWith,
|
|
1586
|
+
} from '@tanstack/solid-router'
|
|
1587
|
+
|
|
1588
|
+
const router = createRouter({
|
|
1589
|
+
parseSearch: parseSearchWith((value) => JSON.parse(decodeFromBinary(value))),
|
|
1590
|
+
stringifySearch: stringifySearchWith((value) =>
|
|
1591
|
+
encodeToBinary(JSON.stringify(value)),
|
|
1592
|
+
),
|
|
1593
|
+
})
|
|
1594
|
+
|
|
1595
|
+
function decodeFromBinary(str: string): string {
|
|
1596
|
+
return decodeURIComponent(
|
|
1597
|
+
Array.prototype.map
|
|
1598
|
+
.call(atob(str), function (c) {
|
|
1599
|
+
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
|
|
1600
|
+
})
|
|
1601
|
+
.join(''),
|
|
1602
|
+
)
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function encodeToBinary(str: string): string {
|
|
1606
|
+
return btoa(
|
|
1607
|
+
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
|
|
1608
|
+
return String.fromCharCode(parseInt(p1, 16))
|
|
1609
|
+
}),
|
|
1610
|
+
)
|
|
1611
|
+
}
|
|
1612
|
+
\`\`\`
|
|
1613
|
+
|
|
1614
|
+
<!-- ::end:framework -->
|
|
1615
|
+
|
|
1151
1616
|
> [⚠️ Why does this snippet not use atob/btoa?](#safe-binary-encodingdecoding)
|
|
1152
1617
|
|
|
1153
1618
|
So, if we were to turn the previous object into a search string using this configuration, it would look like this:
|
|
@@ -1163,6 +1628,10 @@ So, if we were to turn the previous object into a search string using this confi
|
|
|
1163
1628
|
|
|
1164
1629
|
The [query-string](https://github.com/sindresorhus/query-string) library is a popular for being able to reliably parse and stringify query strings. You can use it to customize the serialization format of your search params. This can be done with the following code:
|
|
1165
1630
|
|
|
1631
|
+
<!-- ::start:framework -->
|
|
1632
|
+
|
|
1633
|
+
# React
|
|
1634
|
+
|
|
1166
1635
|
\`\`\`tsx
|
|
1167
1636
|
import { createRouter } from '@tanstack/react-router'
|
|
1168
1637
|
import qs from 'query-string'
|
|
@@ -1182,6 +1651,29 @@ const router = createRouter({
|
|
|
1182
1651
|
})
|
|
1183
1652
|
\`\`\`
|
|
1184
1653
|
|
|
1654
|
+
# Solid
|
|
1655
|
+
|
|
1656
|
+
\`\`\`tsx
|
|
1657
|
+
import { createRouter } from '@tanstack/solid-router'
|
|
1658
|
+
import qs from 'query-string'
|
|
1659
|
+
|
|
1660
|
+
const router = createRouter({
|
|
1661
|
+
// ...
|
|
1662
|
+
stringifySearch: stringifySearchWith((value) =>
|
|
1663
|
+
qs.stringify(value, {
|
|
1664
|
+
// ...options
|
|
1665
|
+
}),
|
|
1666
|
+
),
|
|
1667
|
+
parseSearch: parseSearchWith((value) =>
|
|
1668
|
+
qs.parse(value, {
|
|
1669
|
+
// ...options
|
|
1670
|
+
}),
|
|
1671
|
+
),
|
|
1672
|
+
})
|
|
1673
|
+
\`\`\`
|
|
1674
|
+
|
|
1675
|
+
<!-- ::end:framework -->
|
|
1676
|
+
|
|
1185
1677
|
So, if we were to turn the previous object into a search string using this configuration, it would look like this:
|
|
1186
1678
|
|
|
1187
1679
|
\`\`\`txt
|
|
@@ -1192,6 +1684,10 @@ So, if we were to turn the previous object into a search string using this confi
|
|
|
1192
1684
|
|
|
1193
1685
|
[JSURL2](https://github.com/wmertens/jsurl2) is a non-standard library that can compress URLs while still maintaining readability. This can be done with the following code:
|
|
1194
1686
|
|
|
1687
|
+
<!-- ::start:framework -->
|
|
1688
|
+
|
|
1689
|
+
# React
|
|
1690
|
+
|
|
1195
1691
|
\`\`\`tsx
|
|
1196
1692
|
import {
|
|
1197
1693
|
Router,
|
|
@@ -1207,6 +1703,25 @@ const router = createRouter({
|
|
|
1207
1703
|
})
|
|
1208
1704
|
\`\`\`
|
|
1209
1705
|
|
|
1706
|
+
# Solid
|
|
1707
|
+
|
|
1708
|
+
\`\`\`tsx
|
|
1709
|
+
import {
|
|
1710
|
+
Router,
|
|
1711
|
+
parseSearchWith,
|
|
1712
|
+
stringifySearchWith,
|
|
1713
|
+
} from '@tanstack/solid-router'
|
|
1714
|
+
import { parse, stringify } from 'jsurl2'
|
|
1715
|
+
|
|
1716
|
+
const router = createRouter({
|
|
1717
|
+
// ...
|
|
1718
|
+
parseSearch: parseSearchWith(parse),
|
|
1719
|
+
stringifySearch: stringifySearchWith(stringify),
|
|
1720
|
+
})
|
|
1721
|
+
\`\`\`
|
|
1722
|
+
|
|
1723
|
+
<!-- ::end:framework -->
|
|
1724
|
+
|
|
1210
1725
|
So, if we were to turn the previous object into a search string using this configuration, it would look like this:
|
|
1211
1726
|
|
|
1212
1727
|
\`\`\`txt
|
|
@@ -1217,6 +1732,10 @@ So, if we were to turn the previous object into a search string using this confi
|
|
|
1217
1732
|
|
|
1218
1733
|
[Zipson](https://jgranstrom.github.io/zipson/) is a very user-friendly and performant JSON compression library (both in runtime performance and the resulting compression performance). To compress your search params with it (which requires escaping/unescaping and base64 encoding/decoding them as well), you can use the following code:
|
|
1219
1734
|
|
|
1735
|
+
<!-- ::start:framework -->
|
|
1736
|
+
|
|
1737
|
+
# React
|
|
1738
|
+
|
|
1220
1739
|
\`\`\`tsx
|
|
1221
1740
|
import {
|
|
1222
1741
|
Router,
|
|
@@ -1251,6 +1770,44 @@ function encodeToBinary(str: string): string {
|
|
|
1251
1770
|
}
|
|
1252
1771
|
\`\`\`
|
|
1253
1772
|
|
|
1773
|
+
# Solid
|
|
1774
|
+
|
|
1775
|
+
\`\`\`tsx
|
|
1776
|
+
import {
|
|
1777
|
+
Router,
|
|
1778
|
+
parseSearchWith,
|
|
1779
|
+
stringifySearchWith,
|
|
1780
|
+
} from '@tanstack/solid-router'
|
|
1781
|
+
import { stringify, parse } from 'zipson'
|
|
1782
|
+
|
|
1783
|
+
const router = createRouter({
|
|
1784
|
+
parseSearch: parseSearchWith((value) => parse(decodeFromBinary(value))),
|
|
1785
|
+
stringifySearch: stringifySearchWith((value) =>
|
|
1786
|
+
encodeToBinary(stringify(value)),
|
|
1787
|
+
),
|
|
1788
|
+
})
|
|
1789
|
+
|
|
1790
|
+
function decodeFromBinary(str: string): string {
|
|
1791
|
+
return decodeURIComponent(
|
|
1792
|
+
Array.prototype.map
|
|
1793
|
+
.call(atob(str), function (c) {
|
|
1794
|
+
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
|
|
1795
|
+
})
|
|
1796
|
+
.join(''),
|
|
1797
|
+
)
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
function encodeToBinary(str: string): string {
|
|
1801
|
+
return btoa(
|
|
1802
|
+
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
|
|
1803
|
+
return String.fromCharCode(parseInt(p1, 16))
|
|
1804
|
+
}),
|
|
1805
|
+
)
|
|
1806
|
+
}
|
|
1807
|
+
\`\`\`
|
|
1808
|
+
|
|
1809
|
+
<!-- ::end:framework -->
|
|
1810
|
+
|
|
1254
1811
|
> [⚠️ Why does this snippet not use atob/btoa?](#safe-binary-encodingdecoding)
|
|
1255
1812
|
|
|
1256
1813
|
So, if we were to turn the previous object into a search string using this configuration, it would look like this:
|
|
@@ -1261,13 +1818,13 @@ So, if we were to turn the previous object into a search string using this confi
|
|
|
1261
1818
|
|
|
1262
1819
|
<hr>
|
|
1263
1820
|
|
|
1264
|
-
|
|
1821
|
+
## Safe Binary Encoding/Decoding
|
|
1265
1822
|
|
|
1266
1823
|
In the browser, the \`atob\` and \`btoa\` functions are not guaranteed to work properly with non-UTF8 characters. We recommend using these encoding/decoding utilities instead:
|
|
1267
1824
|
|
|
1268
1825
|
To encode from a string to a binary string:
|
|
1269
1826
|
|
|
1270
|
-
\`\`\`
|
|
1827
|
+
\`\`\`ts
|
|
1271
1828
|
export function encodeToBinary(str: string): string {
|
|
1272
1829
|
return btoa(
|
|
1273
1830
|
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
|
|
@@ -1279,7 +1836,7 @@ export function encodeToBinary(str: string): string {
|
|
|
1279
1836
|
|
|
1280
1837
|
To decode from a binary string to a string:
|
|
1281
1838
|
|
|
1282
|
-
\`\`\`
|
|
1839
|
+
\`\`\`ts
|
|
1283
1840
|
export function decodeFromBinary(str: string): string {
|
|
1284
1841
|
return decodeURIComponent(
|
|
1285
1842
|
Array.prototype.map
|
|
@@ -1350,7 +1907,7 @@ The router cache is built-in and is as easy as returning data from any route's \
|
|
|
1350
1907
|
Route \`loader\` functions are called when a route match is loaded. They are called with a single parameter which is an object containing many helpful properties. We'll go over those in a bit, but first, let's look at an example of a route \`loader\` function:
|
|
1351
1908
|
|
|
1352
1909
|
\`\`\`tsx
|
|
1353
|
-
// routes/posts.tsx
|
|
1910
|
+
// src/routes/posts.tsx
|
|
1354
1911
|
export const Route = createFileRoute('/posts')({
|
|
1355
1912
|
loader: () => fetchPosts(),
|
|
1356
1913
|
})
|
|
@@ -1387,6 +1944,10 @@ const posts = Route.useLoaderData()
|
|
|
1387
1944
|
|
|
1388
1945
|
If you don't have ready access to your route object (i.e. you're deep in the component tree for the current route), you can use \`getRouteApi\` to access the same hook (as well as the other hooks on the Route object). This should be preferred over importing the Route object, which is likely to create circular dependencies.
|
|
1389
1946
|
|
|
1947
|
+
<!-- ::start:framework -->
|
|
1948
|
+
|
|
1949
|
+
# React
|
|
1950
|
+
|
|
1390
1951
|
\`\`\`tsx
|
|
1391
1952
|
import { getRouteApi } from '@tanstack/react-router'
|
|
1392
1953
|
|
|
@@ -1396,6 +1957,19 @@ const routeApi = getRouteApi('/posts')
|
|
|
1396
1957
|
const data = routeApi.useLoaderData()
|
|
1397
1958
|
\`\`\`
|
|
1398
1959
|
|
|
1960
|
+
# Solid
|
|
1961
|
+
|
|
1962
|
+
\`\`\`tsx
|
|
1963
|
+
import { getRouteApi } from '@tanstack/solid-router'
|
|
1964
|
+
|
|
1965
|
+
// in your component
|
|
1966
|
+
|
|
1967
|
+
const routeApi = getRouteApi('/posts')
|
|
1968
|
+
const data = routeApi.useLoaderData()
|
|
1969
|
+
\`\`\`
|
|
1970
|
+
|
|
1971
|
+
<!-- ::end:framework -->
|
|
1972
|
+
|
|
1399
1973
|
## Dependency-based Stale-While-Revalidate Caching
|
|
1400
1974
|
|
|
1401
1975
|
TanStack Router provides a built-in Stale-While-Revalidate caching layer for route loaders that is keyed on the dependencies of a route:
|
|
@@ -1570,6 +2144,10 @@ export const fetchPosts = async () => {
|
|
|
1570
2144
|
|
|
1571
2145
|
- \`/routes/__root.tsx\`
|
|
1572
2146
|
|
|
2147
|
+
<!-- ::start:framework -->
|
|
2148
|
+
|
|
2149
|
+
# React
|
|
2150
|
+
|
|
1573
2151
|
\`\`\`tsx
|
|
1574
2152
|
import { createRootRouteWithContext } from '@tanstack/react-router'
|
|
1575
2153
|
|
|
@@ -1579,11 +2157,22 @@ export const Route = createRootRouteWithContext<{
|
|
|
1579
2157
|
}>()() // NOTE: the double call is on purpose, since createRootRouteWithContext is a factory ;)
|
|
1580
2158
|
\`\`\`
|
|
1581
2159
|
|
|
1582
|
-
|
|
2160
|
+
# Solid
|
|
1583
2161
|
|
|
1584
2162
|
\`\`\`tsx
|
|
1585
|
-
import {
|
|
2163
|
+
import { createRootRouteWithContext } from '@tanstack/solid-router'
|
|
2164
|
+
|
|
2165
|
+
// Create a root route using the createRootRouteWithContext<{...}>() function and pass it whatever types you would like to be available in your router context.
|
|
2166
|
+
export const Route = createRootRouteWithContext<{
|
|
2167
|
+
fetchPosts: typeof fetchPosts
|
|
2168
|
+
}>()() // NOTE: the double call is on purpose, since createRootRouteWithContext is a factory ;)
|
|
2169
|
+
\`\`\`
|
|
1586
2170
|
|
|
2171
|
+
<!-- ::end:framework -->
|
|
2172
|
+
|
|
2173
|
+
- \`/routes/posts.tsx\`
|
|
2174
|
+
|
|
2175
|
+
\`\`\`tsx
|
|
1587
2176
|
// Notice how our postsRoute references context to get our fetchPosts function
|
|
1588
2177
|
// This can be a powerful tool for dependency injection across your router
|
|
1589
2178
|
// and routes.
|
|
@@ -1613,7 +2202,7 @@ const router = createRouter({
|
|
|
1613
2202
|
To use path params in your \`loader\` function, access them via the \`params\` property on the function's parameters. Here's an example:
|
|
1614
2203
|
|
|
1615
2204
|
\`\`\`tsx
|
|
1616
|
-
// routes/posts.$postId.tsx
|
|
2205
|
+
// src/routes/posts.$postId.tsx
|
|
1617
2206
|
export const Route = createFileRoute('/posts/$postId')({
|
|
1618
2207
|
loader: ({ params: { postId } }) => fetchPostById(postId),
|
|
1619
2208
|
})
|
|
@@ -1624,9 +2213,7 @@ export const Route = createFileRoute('/posts/$postId')({
|
|
|
1624
2213
|
Passing down global context to your router is great, but what if you want to provide context that is specific to a route? This is where the \`beforeLoad\` option comes in. The \`beforeLoad\` option is a function that runs right before attempting to load a route and receives the same parameters as \`loader\`. Beyond its ability to redirect potential matches, block loader requests, etc, it can also return an object that will be merged into the route's context. Let's take a look at an example where we inject some data into our route context via the \`beforeLoad\` option:
|
|
1625
2214
|
|
|
1626
2215
|
\`\`\`tsx
|
|
1627
|
-
// /routes/posts.tsx
|
|
1628
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
1629
|
-
|
|
2216
|
+
// src/routes/posts.tsx
|
|
1630
2217
|
export const Route = createFileRoute('/posts')({
|
|
1631
2218
|
// Pass the fetchPosts function to the route context
|
|
1632
2219
|
beforeLoad: () => ({
|
|
@@ -1688,7 +2275,7 @@ export const Route = createFileRoute('/posts')({
|
|
|
1688
2275
|
The \`abortController\` property of the \`loader\` function is an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). Its signal is cancelled when the route is unloaded or when the \`loader\` call becomes outdated. This is useful for cancelling network requests when the route is unloaded or when the route's params change. Here is an example using it with a fetch call:
|
|
1689
2276
|
|
|
1690
2277
|
\`\`\`tsx
|
|
1691
|
-
// routes/posts.tsx
|
|
2278
|
+
// src/routes/posts.tsx
|
|
1692
2279
|
export const Route = createFileRoute('/posts')({
|
|
1693
2280
|
loader: ({ abortController }) =>
|
|
1694
2281
|
fetchPosts({
|
|
@@ -1703,7 +2290,7 @@ export const Route = createFileRoute('/posts')({
|
|
|
1703
2290
|
The \`preload\` property of the \`loader\` function is a boolean which is \`true\` when the route is being preloaded instead of loaded. Some data loading libraries may handle preloading differently than a standard fetch, so you may want to pass \`preload\` to your data loading library, or use it to execute the appropriate data loading logic:
|
|
1704
2291
|
|
|
1705
2292
|
\`\`\`tsx
|
|
1706
|
-
// routes/posts.tsx
|
|
2293
|
+
// src/routes/posts.tsx
|
|
1707
2294
|
export const Route = createFileRoute('/posts')({
|
|
1708
2295
|
loader: async ({ preload }) =>
|
|
1709
2296
|
fetchPosts({
|
|
@@ -1744,7 +2331,7 @@ TanStack Router provides a few ways to handle errors that occur during the route
|
|
|
1744
2331
|
The \`routeOptions.onError\` option is a function that is called when an error occurs during the route loading.
|
|
1745
2332
|
|
|
1746
2333
|
\`\`\`tsx
|
|
1747
|
-
// routes/posts.tsx
|
|
2334
|
+
// src/routes/posts.tsx
|
|
1748
2335
|
export const Route = createFileRoute('/posts')({
|
|
1749
2336
|
loader: () => fetchPosts(),
|
|
1750
2337
|
onError: ({ error }) => {
|
|
@@ -1759,7 +2346,7 @@ export const Route = createFileRoute('/posts')({
|
|
|
1759
2346
|
The \`routeOptions.onCatch\` option is a function that is called whenever an error was caught by the router's CatchBoundary.
|
|
1760
2347
|
|
|
1761
2348
|
\`\`\`tsx
|
|
1762
|
-
// routes/posts.tsx
|
|
2349
|
+
// src/routes/posts.tsx
|
|
1763
2350
|
export const Route = createFileRoute('/posts')({
|
|
1764
2351
|
onCatch: ({ error, errorInfo }) => {
|
|
1765
2352
|
// Log the error
|
|
@@ -1776,7 +2363,7 @@ The \`routeOptions.errorComponent\` option is a component that is rendered when
|
|
|
1776
2363
|
- \`reset\` - A function to reset the internal \`CatchBoundary\`
|
|
1777
2364
|
|
|
1778
2365
|
\`\`\`tsx
|
|
1779
|
-
// routes/posts.tsx
|
|
2366
|
+
// src/routes/posts.tsx
|
|
1780
2367
|
export const Route = createFileRoute('/posts')({
|
|
1781
2368
|
loader: () => fetchPosts(),
|
|
1782
2369
|
errorComponent: ({ error }) => {
|
|
@@ -1789,7 +2376,7 @@ export const Route = createFileRoute('/posts')({
|
|
|
1789
2376
|
The \`reset\` function can be used to allow the user to retry rendering the error boundaries normal children:
|
|
1790
2377
|
|
|
1791
2378
|
\`\`\`tsx
|
|
1792
|
-
// routes/posts.tsx
|
|
2379
|
+
// src/routes/posts.tsx
|
|
1793
2380
|
export const Route = createFileRoute('/posts')({
|
|
1794
2381
|
loader: () => fetchPosts(),
|
|
1795
2382
|
errorComponent: ({ error, reset }) => {
|
|
@@ -1813,7 +2400,7 @@ export const Route = createFileRoute('/posts')({
|
|
|
1813
2400
|
If the error was the result of a route load, you should instead call \`router.invalidate()\`, which will coordinate both a router reload and an error boundary reset:
|
|
1814
2401
|
|
|
1815
2402
|
\`\`\`tsx
|
|
1816
|
-
// routes/posts.tsx
|
|
2403
|
+
// src/routes/posts.tsx
|
|
1817
2404
|
export const Route = createFileRoute('/posts')({
|
|
1818
2405
|
loader: () => fetchPosts(),
|
|
1819
2406
|
errorComponent: ({ error, reset }) => {
|
|
@@ -1841,9 +2428,7 @@ export const Route = createFileRoute('/posts')({
|
|
|
1841
2428
|
TanStack Router provides a default \`ErrorComponent\` that is rendered when an error occurs during the route loading or rendering lifecycle. If you choose to override your routes' error components, it's still wise to always fall back to rendering any uncaught errors with the default \`ErrorComponent\`:
|
|
1842
2429
|
|
|
1843
2430
|
\`\`\`tsx
|
|
1844
|
-
// routes/posts.tsx
|
|
1845
|
-
import { createFileRoute, ErrorComponent } from '@tanstack/react-router'
|
|
1846
|
-
|
|
2431
|
+
// src/routes/posts.tsx
|
|
1847
2432
|
export const Route = createFileRoute('/posts')({
|
|
1848
2433
|
loader: () => fetchPosts(),
|
|
1849
2434
|
errorComponent: ({ error }) => {
|
|
@@ -2064,12 +2649,14 @@ The \`Await\` component resolves the promise by triggering the nearest suspense
|
|
|
2064
2649
|
|
|
2065
2650
|
If the promise is rejected, the \`Await\` component will throw the serialized error, which can be caught by the nearest error boundary.
|
|
2066
2651
|
|
|
2067
|
-
|
|
2652
|
+
<!-- ::start:framework -->
|
|
2653
|
+
|
|
2654
|
+
# React
|
|
2068
2655
|
|
|
2069
2656
|
> [!TIP]
|
|
2070
2657
|
> In React 19, you can use the \`use()\` hook instead of \`Await\`
|
|
2071
2658
|
|
|
2072
|
-
|
|
2659
|
+
<!-- ::end:framework -->
|
|
2073
2660
|
|
|
2074
2661
|
## Deferred Data Loading with External libraries
|
|
2075
2662
|
|
|
@@ -2077,6 +2664,10 @@ When your strategy for fetching information for the route relies on [External Da
|
|
|
2077
2664
|
|
|
2078
2665
|
So, instead of using \`defer\` and \`Await\`, you'll instead want to use the Route's \`loader\` to kick off the data fetching and then use the library's hooks to access the data in your components.
|
|
2079
2666
|
|
|
2667
|
+
<!-- ::start:framework -->
|
|
2668
|
+
|
|
2669
|
+
# React
|
|
2670
|
+
|
|
2080
2671
|
\`\`\`tsx
|
|
2081
2672
|
// src/routes/posts.$postId.tsx
|
|
2082
2673
|
import { createFileRoute } from '@tanstack/react-router'
|
|
@@ -2093,8 +2684,32 @@ export const Route = createFileRoute('/posts/$postId')({
|
|
|
2093
2684
|
})
|
|
2094
2685
|
\`\`\`
|
|
2095
2686
|
|
|
2687
|
+
# Solid
|
|
2688
|
+
|
|
2689
|
+
\`\`\`tsx
|
|
2690
|
+
// src/routes/posts.$postId.tsx
|
|
2691
|
+
import { createFileRoute } from '@tanstack/solid-router'
|
|
2692
|
+
import { slowDataOptions, fastDataOptions } from '~/api/query-options'
|
|
2693
|
+
|
|
2694
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
2695
|
+
loader: async ({ context: { queryClient } }) => {
|
|
2696
|
+
// Kick off the fetching of some slower data, but do not await it
|
|
2697
|
+
queryClient.prefetchQuery(slowDataOptions())
|
|
2698
|
+
|
|
2699
|
+
// Fetch and await some data that resolves quickly
|
|
2700
|
+
await queryClient.ensureQueryData(fastDataOptions())
|
|
2701
|
+
},
|
|
2702
|
+
})
|
|
2703
|
+
\`\`\`
|
|
2704
|
+
|
|
2705
|
+
<!-- ::end:framework -->
|
|
2706
|
+
|
|
2096
2707
|
Then in your component, you can use the library's hooks to access the data:
|
|
2097
2708
|
|
|
2709
|
+
<!-- ::start:framework -->
|
|
2710
|
+
|
|
2711
|
+
# React
|
|
2712
|
+
|
|
2098
2713
|
\`\`\`tsx
|
|
2099
2714
|
// src/routes/posts.$postId.tsx
|
|
2100
2715
|
import { createFileRoute } from '@tanstack/react-router'
|
|
@@ -2125,11 +2740,47 @@ function SlowDataComponent() {
|
|
|
2125
2740
|
}
|
|
2126
2741
|
\`\`\`
|
|
2127
2742
|
|
|
2743
|
+
# Solid
|
|
2744
|
+
|
|
2745
|
+
\`\`\`tsx
|
|
2746
|
+
// src/routes/posts.$postId.tsx
|
|
2747
|
+
import { createFileRoute } from '@tanstack/solid-router'
|
|
2748
|
+
import { useSuspenseQuery } from '@tanstack/solid-query'
|
|
2749
|
+
import { slowDataOptions, fastDataOptions } from '~/api/query-options'
|
|
2750
|
+
|
|
2751
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
2752
|
+
// ...
|
|
2753
|
+
component: PostIdComponent,
|
|
2754
|
+
})
|
|
2755
|
+
|
|
2756
|
+
function PostIdComponent() {
|
|
2757
|
+
const fastData = useSuspenseQuery(fastDataOptions())
|
|
2758
|
+
|
|
2759
|
+
// do something with fastData
|
|
2760
|
+
|
|
2761
|
+
return (
|
|
2762
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
2763
|
+
<SlowDataComponent />
|
|
2764
|
+
</Suspense>
|
|
2765
|
+
)
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
function SlowDataComponent() {
|
|
2769
|
+
const data = useSuspenseQuery(slowDataOptions())
|
|
2770
|
+
|
|
2771
|
+
return <div>{data()}</div>
|
|
2772
|
+
}
|
|
2773
|
+
\`\`\`
|
|
2774
|
+
|
|
2775
|
+
<!-- ::end:framework -->
|
|
2776
|
+
|
|
2128
2777
|
## Caching and Invalidation
|
|
2129
2778
|
|
|
2130
2779
|
Streamed promises follow the same lifecycle as the loader data they are associated with. They can even be preloaded!
|
|
2131
2780
|
|
|
2132
|
-
|
|
2781
|
+
<!-- ::start:framework -->
|
|
2782
|
+
|
|
2783
|
+
# React
|
|
2133
2784
|
|
|
2134
2785
|
## SSR & Streaming Deferred Data
|
|
2135
2786
|
|
|
@@ -2155,17 +2806,17 @@ The following is a high-level overview of how deferred data streaming works with
|
|
|
2155
2806
|
- Client
|
|
2156
2807
|
- The suspended placeholder promises within \`<Await>\` are resolved with the streamed data/error responses and either render the result or throw the error to the nearest error boundary
|
|
2157
2808
|
|
|
2158
|
-
|
|
2809
|
+
<!-- ::end:framework -->
|
|
2159
2810
|
|
|
2160
2811
|
# Document Head Management
|
|
2161
2812
|
|
|
2162
|
-
Document head management is the process of managing the head, title, meta, link, and script tags of a document and TanStack Router provides a robust way to manage the document head for full-stack applications that use Start and for single-page applications that use
|
|
2813
|
+
Document head management is the process of managing the head, title, meta, link, and script tags of a document and TanStack Router provides a robust way to manage the document head for full-stack applications that use Start and for single-page applications that use TanStack Router. It provides:
|
|
2163
2814
|
|
|
2164
2815
|
- Automatic deduping of \`title\` and \`meta\` tags
|
|
2165
2816
|
- Automatic loading/unloading of tags based on route visibility
|
|
2166
2817
|
- A composable way to merge \`title\` and \`meta\` tags from nested routes
|
|
2167
2818
|
|
|
2168
|
-
For full-stack applications that use Start, and even for single-page applications that use
|
|
2819
|
+
For full-stack applications that use Start, and even for single-page applications that use TanStack Router, managing the document head is a crucial part of any application for the following reasons:
|
|
2169
2820
|
|
|
2170
2821
|
- SEO
|
|
2171
2822
|
- Social media sharing
|
|
@@ -2227,6 +2878,10 @@ It should be **rendered either in the \`<head>\` tag of your root layout or as h
|
|
|
2227
2878
|
|
|
2228
2879
|
### Start/Full-Stack Applications
|
|
2229
2880
|
|
|
2881
|
+
<!-- ::start:framework -->
|
|
2882
|
+
|
|
2883
|
+
# React
|
|
2884
|
+
|
|
2230
2885
|
\`\`\`tsx
|
|
2231
2886
|
import { HeadContent } from '@tanstack/react-router'
|
|
2232
2887
|
|
|
@@ -2244,10 +2899,35 @@ export const Route = createRootRoute({
|
|
|
2244
2899
|
})
|
|
2245
2900
|
\`\`\`
|
|
2246
2901
|
|
|
2902
|
+
# Solid
|
|
2903
|
+
|
|
2904
|
+
\`\`\`tsx
|
|
2905
|
+
import { HeadContent } from '@tanstack/solid-router'
|
|
2906
|
+
|
|
2907
|
+
export const Route = createRootRoute({
|
|
2908
|
+
component: () => (
|
|
2909
|
+
<html>
|
|
2910
|
+
<head>
|
|
2911
|
+
<HeadContent />
|
|
2912
|
+
</head>
|
|
2913
|
+
<body>
|
|
2914
|
+
<Outlet />
|
|
2915
|
+
</body>
|
|
2916
|
+
</html>
|
|
2917
|
+
),
|
|
2918
|
+
})
|
|
2919
|
+
\`\`\`
|
|
2920
|
+
|
|
2921
|
+
<!-- ::end:framework -->
|
|
2922
|
+
|
|
2247
2923
|
### Single-Page Applications
|
|
2248
2924
|
|
|
2249
2925
|
First, remove the \`<title>\` tag from the index.html if you have set any.
|
|
2250
2926
|
|
|
2927
|
+
<!-- ::start:framework -->
|
|
2928
|
+
|
|
2929
|
+
# React
|
|
2930
|
+
|
|
2251
2931
|
\`\`\`tsx
|
|
2252
2932
|
import { HeadContent } from '@tanstack/react-router'
|
|
2253
2933
|
|
|
@@ -2261,6 +2941,23 @@ const rootRoute = createRootRoute({
|
|
|
2261
2941
|
})
|
|
2262
2942
|
\`\`\`
|
|
2263
2943
|
|
|
2944
|
+
# Solid
|
|
2945
|
+
|
|
2946
|
+
\`\`\`tsx
|
|
2947
|
+
import { HeadContent } from '@tanstack/solid-router'
|
|
2948
|
+
|
|
2949
|
+
const rootRoute = createRootRoute({
|
|
2950
|
+
component: () => (
|
|
2951
|
+
<>
|
|
2952
|
+
<HeadContent />
|
|
2953
|
+
<Outlet />
|
|
2954
|
+
</>
|
|
2955
|
+
),
|
|
2956
|
+
})
|
|
2957
|
+
\`\`\`
|
|
2958
|
+
|
|
2959
|
+
<!-- ::end:framework -->
|
|
2960
|
+
|
|
2264
2961
|
## Managing Body Scripts
|
|
2265
2962
|
|
|
2266
2963
|
In addition to scripts that can be rendered in the \`<head>\` tag, you can also render scripts in the \`<body>\` tag using the \`routeOptions.scripts\` property. This is useful for loading scripts (even inline scripts) that require the DOM to be loaded, but before the main entry point of your application (which includes hydration if you're using Start or a full-stack implementation of TanStack Router).
|
|
@@ -2286,9 +2983,13 @@ The \`<Scripts />\` component is **required** to render the body scripts of a do
|
|
|
2286
2983
|
|
|
2287
2984
|
### Example
|
|
2288
2985
|
|
|
2986
|
+
<!-- ::start:framework -->
|
|
2987
|
+
|
|
2988
|
+
# React
|
|
2989
|
+
|
|
2289
2990
|
\`\`\`tsx
|
|
2290
|
-
import {
|
|
2291
|
-
export const
|
|
2991
|
+
import { createRootRoute, Scripts } from '@tanstack/react-router'
|
|
2992
|
+
export const Route = createFileRoute('/')({
|
|
2292
2993
|
component: () => (
|
|
2293
2994
|
<html>
|
|
2294
2995
|
<head />
|
|
@@ -2301,23 +3002,33 @@ export const Router = createFileRoute('/')({
|
|
|
2301
3002
|
})
|
|
2302
3003
|
\`\`\`
|
|
2303
3004
|
|
|
2304
|
-
|
|
2305
|
-
import { Scripts, createRootRoute } from '@tanstack/react-router'
|
|
3005
|
+
# Solid
|
|
2306
3006
|
|
|
2307
|
-
|
|
3007
|
+
\`\`\`tsx
|
|
3008
|
+
import { createFileRoute, Scripts } from '@tanstack/solid-router'
|
|
3009
|
+
export const Route = createRootRoute('/')({
|
|
2308
3010
|
component: () => (
|
|
2309
|
-
|
|
2310
|
-
<
|
|
2311
|
-
<
|
|
2312
|
-
|
|
3011
|
+
<html>
|
|
3012
|
+
<head />
|
|
3013
|
+
<body>
|
|
3014
|
+
<Outlet />
|
|
3015
|
+
<Scripts />
|
|
3016
|
+
</body>
|
|
3017
|
+
</html>
|
|
2313
3018
|
),
|
|
2314
3019
|
})
|
|
2315
3020
|
\`\`\`
|
|
2316
3021
|
|
|
3022
|
+
<!-- ::end:framework -->
|
|
3023
|
+
|
|
2317
3024
|
## Inline Scripts with ScriptOnce
|
|
2318
3025
|
|
|
2319
3026
|
For scripts that must run before React hydrates (like theme detection), use \`ScriptOnce\`. This is particularly useful for avoiding flash of unstyled content (FOUC) or theme flicker.
|
|
2320
3027
|
|
|
3028
|
+
<!-- ::start:framework -->
|
|
3029
|
+
|
|
3030
|
+
# React
|
|
3031
|
+
|
|
2321
3032
|
\`\`\`tsx
|
|
2322
3033
|
import { ScriptOnce } from '@tanstack/react-router'
|
|
2323
3034
|
|
|
@@ -2341,6 +3052,33 @@ function ThemeProvider({ children }) {
|
|
|
2341
3052
|
}
|
|
2342
3053
|
\`\`\`
|
|
2343
3054
|
|
|
3055
|
+
# Solid
|
|
3056
|
+
|
|
3057
|
+
\`\`\`tsx
|
|
3058
|
+
import { ScriptOnce } from '@tanstack/solid-router'
|
|
3059
|
+
|
|
3060
|
+
const themeScript = \`(function() {
|
|
3061
|
+
try {
|
|
3062
|
+
const theme = localStorage.getItem('theme') || 'auto';
|
|
3063
|
+
const resolved = theme === 'auto'
|
|
3064
|
+
? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
|
3065
|
+
: theme;
|
|
3066
|
+
document.documentElement.classList.add(resolved);
|
|
3067
|
+
} catch (e) {}
|
|
3068
|
+
})();\`
|
|
3069
|
+
|
|
3070
|
+
function ThemeProvider({ children }) {
|
|
3071
|
+
return (
|
|
3072
|
+
<>
|
|
3073
|
+
<ScriptOnce children={themeScript} />
|
|
3074
|
+
{children}
|
|
3075
|
+
</>
|
|
3076
|
+
)
|
|
3077
|
+
}
|
|
3078
|
+
\`\`\`
|
|
3079
|
+
|
|
3080
|
+
<!-- ::end:framework -->
|
|
3081
|
+
|
|
2344
3082
|
### How ScriptOnce Works
|
|
2345
3083
|
|
|
2346
3084
|
1. During SSR, renders a \`<script>\` tag with the provided code
|
|
@@ -2348,6 +3086,10 @@ function ThemeProvider({ children }) {
|
|
|
2348
3086
|
3. After execution, the script removes itself from the DOM
|
|
2349
3087
|
4. On client-side navigation, nothing is rendered (prevents duplicate execution)
|
|
2350
3088
|
|
|
3089
|
+
<!-- ::start:framework -->
|
|
3090
|
+
|
|
3091
|
+
# React
|
|
3092
|
+
|
|
2351
3093
|
### Preventing Hydration Warnings
|
|
2352
3094
|
|
|
2353
3095
|
If your script modifies the DOM before hydration (like adding a class to \`<html>\`), use \`suppressHydrationWarning\` to prevent React warnings:
|
|
@@ -2370,6 +3112,8 @@ export const Route = createRootRoute({
|
|
|
2370
3112
|
})
|
|
2371
3113
|
\`\`\`
|
|
2372
3114
|
|
|
3115
|
+
<!-- ::end:framework -->
|
|
3116
|
+
|
|
2373
3117
|
### Common Use Cases
|
|
2374
3118
|
|
|
2375
3119
|
- **Theme/dark mode detection** - Apply theme class before hydration to prevent flash
|
|
@@ -2575,6 +3319,10 @@ If you don't create a history instance, a browser-oriented instance of this API
|
|
|
2575
3319
|
|
|
2576
3320
|
Once you have a history instance, you can pass it to the \`Router\` constructor:
|
|
2577
3321
|
|
|
3322
|
+
<!-- ::start:framework -->
|
|
3323
|
+
|
|
3324
|
+
# React
|
|
3325
|
+
|
|
2578
3326
|
\`\`\`ts
|
|
2579
3327
|
import { createMemoryHistory, createRouter } from '@tanstack/react-router'
|
|
2580
3328
|
|
|
@@ -2585,6 +3333,20 @@ const memoryHistory = createMemoryHistory({
|
|
|
2585
3333
|
const router = createRouter({ routeTree, history: memoryHistory })
|
|
2586
3334
|
\`\`\`
|
|
2587
3335
|
|
|
3336
|
+
# Solid
|
|
3337
|
+
|
|
3338
|
+
\`\`\`ts
|
|
3339
|
+
import { createMemoryHistory, createRouter } from '@tanstack/solid-router'
|
|
3340
|
+
|
|
3341
|
+
const memoryHistory = createMemoryHistory({
|
|
3342
|
+
initialEntries: ['/'], // Pass your initial url
|
|
3343
|
+
})
|
|
3344
|
+
|
|
3345
|
+
const router = createRouter({ routeTree, history: memoryHistory })
|
|
3346
|
+
\`\`\`
|
|
3347
|
+
|
|
3348
|
+
<!-- ::end:framework -->
|
|
3349
|
+
|
|
2588
3350
|
## Browser Routing
|
|
2589
3351
|
|
|
2590
3352
|
The \`createBrowserHistory\` is the default history type. It uses the browser's history API to manage the browser history.
|
|
@@ -2593,6 +3355,10 @@ The \`createBrowserHistory\` is the default history type. It uses the browser's
|
|
|
2593
3355
|
|
|
2594
3356
|
Hash routing can be helpful if your server doesn't support rewrites to index.html for HTTP requests (among other environments that don't have a server).
|
|
2595
3357
|
|
|
3358
|
+
<!-- ::start:framework -->
|
|
3359
|
+
|
|
3360
|
+
# React
|
|
3361
|
+
|
|
2596
3362
|
\`\`\`ts
|
|
2597
3363
|
import { createHashHistory, createRouter } from '@tanstack/react-router'
|
|
2598
3364
|
|
|
@@ -2601,10 +3367,26 @@ const hashHistory = createHashHistory()
|
|
|
2601
3367
|
const router = createRouter({ routeTree, history: hashHistory })
|
|
2602
3368
|
\`\`\`
|
|
2603
3369
|
|
|
3370
|
+
# Solid
|
|
3371
|
+
|
|
3372
|
+
\`\`\`ts
|
|
3373
|
+
import { createHashHistory, createRouter } from '@tanstack/solid-router'
|
|
3374
|
+
|
|
3375
|
+
const hashHistory = createHashHistory()
|
|
3376
|
+
|
|
3377
|
+
const router = createRouter({ routeTree, history: hashHistory })
|
|
3378
|
+
\`\`\`
|
|
3379
|
+
|
|
3380
|
+
<!-- ::end:framework -->
|
|
3381
|
+
|
|
2604
3382
|
## Memory Routing
|
|
2605
3383
|
|
|
2606
3384
|
Memory routing is useful in environments that are not a browser or when you do not want components to interact with the URL.
|
|
2607
3385
|
|
|
3386
|
+
<!-- ::start:framework -->
|
|
3387
|
+
|
|
3388
|
+
# React
|
|
3389
|
+
|
|
2608
3390
|
\`\`\`ts
|
|
2609
3391
|
import { createMemoryHistory, createRouter } from '@tanstack/react-router'
|
|
2610
3392
|
|
|
@@ -2615,7 +3397,21 @@ const memoryHistory = createMemoryHistory({
|
|
|
2615
3397
|
const router = createRouter({ routeTree, history: memoryHistory })
|
|
2616
3398
|
\`\`\`
|
|
2617
3399
|
|
|
2618
|
-
|
|
3400
|
+
# Solid
|
|
3401
|
+
|
|
3402
|
+
\`\`\`ts
|
|
3403
|
+
import { createMemoryHistory, createRouter } from '@tanstack/solid-router'
|
|
3404
|
+
|
|
3405
|
+
const memoryHistory = createMemoryHistory({
|
|
3406
|
+
initialEntries: ['/'], // Pass your initial url
|
|
3407
|
+
})
|
|
3408
|
+
|
|
3409
|
+
const router = createRouter({ routeTree, history: memoryHistory })
|
|
3410
|
+
\`\`\`
|
|
3411
|
+
|
|
3412
|
+
<!-- ::end:framework -->
|
|
3413
|
+
|
|
3414
|
+
Refer to the [SSR Guide](./ssr.md#automatic-server-history) for usage on the server for server-side rendering.
|
|
2619
3415
|
|
|
2620
3416
|
# Internationalization (i18n)
|
|
2621
3417
|
|
|
@@ -3015,7 +3811,9 @@ There are 2 ways to use navigation blocking:
|
|
|
3015
3811
|
|
|
3016
3812
|
Let's imagine we want to prevent navigation if a form is dirty. We can do this by using the \`useBlocker\` hook:
|
|
3017
3813
|
|
|
3018
|
-
|
|
3814
|
+
<!-- ::start:framework -->
|
|
3815
|
+
|
|
3816
|
+
# React
|
|
3019
3817
|
|
|
3020
3818
|
\`\`\`tsx
|
|
3021
3819
|
import { useBlocker } from '@tanstack/react-router'
|
|
@@ -3036,10 +3834,35 @@ function MyComponent() {
|
|
|
3036
3834
|
}
|
|
3037
3835
|
\`\`\`
|
|
3038
3836
|
|
|
3039
|
-
|
|
3837
|
+
# Solid
|
|
3838
|
+
|
|
3839
|
+
\`\`\`tsx
|
|
3840
|
+
import { useBlocker } from '@tanstack/solid-router'
|
|
3841
|
+
|
|
3842
|
+
function MyComponent() {
|
|
3843
|
+
const [formIsDirty, setFormIsDirty] = createSignal(false)
|
|
3844
|
+
|
|
3845
|
+
useBlocker({
|
|
3846
|
+
shouldBlockFn: () => {
|
|
3847
|
+
if (!formIsDirty()) return false
|
|
3848
|
+
|
|
3849
|
+
const shouldLeave = confirm('Are you sure you want to leave?')
|
|
3850
|
+
return !shouldLeave
|
|
3851
|
+
},
|
|
3852
|
+
})
|
|
3853
|
+
|
|
3854
|
+
// ...
|
|
3855
|
+
}
|
|
3856
|
+
\`\`\`
|
|
3857
|
+
|
|
3858
|
+
<!-- ::end:framework -->
|
|
3040
3859
|
|
|
3041
3860
|
\`shouldBlockFn\` gives you type safe access to the \`current\` and \`next\` location:
|
|
3042
3861
|
|
|
3862
|
+
<!-- ::start:framework -->
|
|
3863
|
+
|
|
3864
|
+
# React
|
|
3865
|
+
|
|
3043
3866
|
\`\`\`tsx
|
|
3044
3867
|
import { useBlocker } from '@tanstack/react-router'
|
|
3045
3868
|
|
|
@@ -3061,9 +3884,36 @@ function MyComponent() {
|
|
|
3061
3884
|
}
|
|
3062
3885
|
\`\`\`
|
|
3063
3886
|
|
|
3887
|
+
# Solid
|
|
3888
|
+
|
|
3889
|
+
\`\`\`tsx
|
|
3890
|
+
import { useBlocker } from '@tanstack/solid-router'
|
|
3891
|
+
|
|
3892
|
+
function MyComponent() {
|
|
3893
|
+
// always block going from /foo to /bar/123?hello=world
|
|
3894
|
+
const { proceed, reset, status } = useBlocker({
|
|
3895
|
+
shouldBlockFn: ({ current, next }) => {
|
|
3896
|
+
return (
|
|
3897
|
+
current.routeId === '/foo' &&
|
|
3898
|
+
next.fullPath === '/bar/$id' &&
|
|
3899
|
+
next.params.id === 123 &&
|
|
3900
|
+
next.search.hello === 'world'
|
|
3901
|
+
)
|
|
3902
|
+
},
|
|
3903
|
+
withResolver: true,
|
|
3904
|
+
})
|
|
3905
|
+
|
|
3906
|
+
// ...
|
|
3907
|
+
}
|
|
3908
|
+
\`\`\`
|
|
3909
|
+
|
|
3910
|
+
<!-- ::end:framework -->
|
|
3911
|
+
|
|
3064
3912
|
Note that even if \`shouldBlockFn\` returns \`false\`, the browser's \`beforeunload\` event may still be triggered on page reloads or tab closing. To gain control over this, you can use the \`enableBeforeUnload\` option to conditionally register the \`beforeunload\` handler:
|
|
3065
3913
|
|
|
3066
|
-
|
|
3914
|
+
<!-- ::start:framework -->
|
|
3915
|
+
|
|
3916
|
+
# React
|
|
3067
3917
|
|
|
3068
3918
|
\`\`\`tsx
|
|
3069
3919
|
import { useBlocker } from '@tanstack/react-router'
|
|
@@ -3080,16 +3930,37 @@ function MyComponent() {
|
|
|
3080
3930
|
}
|
|
3081
3931
|
\`\`\`
|
|
3082
3932
|
|
|
3933
|
+
# Solid
|
|
3934
|
+
|
|
3935
|
+
\`\`\`tsx
|
|
3936
|
+
import { useBlocker } from '@tanstack/solid-router'
|
|
3937
|
+
|
|
3938
|
+
function MyComponent() {
|
|
3939
|
+
const [formIsDirty, setFormIsDirty] = useState(false)
|
|
3940
|
+
|
|
3941
|
+
useBlocker({
|
|
3942
|
+
{/* ... */}
|
|
3943
|
+
enableBeforeUnload: formIsDirty(),
|
|
3944
|
+
})
|
|
3945
|
+
|
|
3946
|
+
// ...
|
|
3947
|
+
}
|
|
3948
|
+
\`\`\`
|
|
3949
|
+
|
|
3950
|
+
<!-- ::end:framework -->
|
|
3951
|
+
|
|
3083
3952
|
You can find more information about the \`useBlocker\` hook in the [API reference](../api/router/useBlockerHook.md).
|
|
3084
3953
|
|
|
3085
3954
|
## Component-based blocking
|
|
3086
3955
|
|
|
3087
3956
|
In addition to logical/hook based blocking, you can use the \`Block\` component to achieve similar results:
|
|
3088
3957
|
|
|
3089
|
-
|
|
3958
|
+
<!-- ::start:framework -->
|
|
3959
|
+
|
|
3960
|
+
# React
|
|
3090
3961
|
|
|
3091
3962
|
\`\`\`tsx
|
|
3092
|
-
import { Block } from '@tanstack/
|
|
3963
|
+
import { Block } from '@tanstack/solid-router'
|
|
3093
3964
|
|
|
3094
3965
|
function MyComponent() {
|
|
3095
3966
|
const [formIsDirty, setFormIsDirty] = useState(false)
|
|
@@ -3120,21 +3991,52 @@ function MyComponent() {
|
|
|
3120
3991
|
}
|
|
3121
3992
|
\`\`\`
|
|
3122
3993
|
|
|
3123
|
-
|
|
3994
|
+
# Solid
|
|
3124
3995
|
|
|
3125
|
-
|
|
3996
|
+
\`\`\`tsx
|
|
3997
|
+
import { Block } from '@tanstack/solid-router'
|
|
3126
3998
|
|
|
3127
|
-
|
|
3999
|
+
function MyComponent() {
|
|
4000
|
+
const [formIsDirty, setFormIsDirty] = createSignal(false)
|
|
3128
4001
|
|
|
3129
|
-
|
|
4002
|
+
return (
|
|
4003
|
+
<Block
|
|
4004
|
+
shouldBlockFn={() => {
|
|
4005
|
+
if (!formIsDirty()) return false
|
|
3130
4006
|
|
|
3131
|
-
|
|
4007
|
+
const shouldLeave = confirm('Are you sure you want to leave?')
|
|
4008
|
+
return !shouldLeave
|
|
4009
|
+
}}
|
|
4010
|
+
/>
|
|
4011
|
+
)
|
|
3132
4012
|
|
|
3133
|
-
|
|
4013
|
+
// OR
|
|
3134
4014
|
|
|
3135
|
-
|
|
4015
|
+
return (
|
|
4016
|
+
<Block shouldBlockFn={() => !formIsDirty} withResolver>
|
|
4017
|
+
{({ status, proceed, reset }) => <>{/* ... */}</>}
|
|
4018
|
+
</Block>
|
|
4019
|
+
)
|
|
4020
|
+
}
|
|
4021
|
+
\`\`\`
|
|
3136
4022
|
|
|
3137
|
-
|
|
4023
|
+
<!-- ::end:framework -->
|
|
4024
|
+
|
|
4025
|
+
## How can I show a custom UI?
|
|
4026
|
+
|
|
4027
|
+
In most cases, using \`window.confirm\` in the \`shouldBlockFn\` function with \`withResolver: false\` in the hook is enough since it will clearly show the user that the navigation is being blocked and resolve the blocking based on their response.
|
|
4028
|
+
|
|
4029
|
+
However, in some situations, you might want to show a custom UI that is intentionally less disruptive and more integrated with your app's design.
|
|
4030
|
+
|
|
4031
|
+
**Note:** The return value of \`shouldBlockFn\` does not resolve the blocking if \`withResolver\` is \`true\`.
|
|
4032
|
+
|
|
4033
|
+
### Hook/logical-based custom UI with resolver
|
|
4034
|
+
|
|
4035
|
+
<!-- ::start:framework -->
|
|
4036
|
+
|
|
4037
|
+
# React
|
|
4038
|
+
|
|
4039
|
+
\`\`\`tsx
|
|
3138
4040
|
import { useBlocker } from '@tanstack/react-router'
|
|
3139
4041
|
|
|
3140
4042
|
function MyComponent() {
|
|
@@ -3161,11 +4063,42 @@ function MyComponent() {
|
|
|
3161
4063
|
}
|
|
3162
4064
|
\`\`\`
|
|
3163
4065
|
|
|
3164
|
-
|
|
4066
|
+
# Solid
|
|
4067
|
+
|
|
4068
|
+
\`\`\`tsx
|
|
4069
|
+
import { useBlocker } from '@tanstack/solid-router'
|
|
4070
|
+
|
|
4071
|
+
function MyComponent() {
|
|
4072
|
+
const [formIsDirty, setFormIsDirty] = createSignal(false)
|
|
4073
|
+
|
|
4074
|
+
const { proceed, reset, status } = useBlocker({
|
|
4075
|
+
shouldBlockFn: () => formIsDirty(),
|
|
4076
|
+
withResolver: true,
|
|
4077
|
+
})
|
|
4078
|
+
|
|
4079
|
+
// ...
|
|
4080
|
+
|
|
4081
|
+
return (
|
|
4082
|
+
<>
|
|
4083
|
+
{/* ... */}
|
|
4084
|
+
{status === 'blocked' && (
|
|
4085
|
+
<div>
|
|
4086
|
+
<p>Are you sure you want to leave?</p>
|
|
4087
|
+
<button onClick={proceed}>Yes</button>
|
|
4088
|
+
<button onClick={reset}>No</button>
|
|
4089
|
+
</div>
|
|
4090
|
+
)}
|
|
4091
|
+
</>
|
|
4092
|
+
}
|
|
4093
|
+
\`\`\`
|
|
4094
|
+
|
|
4095
|
+
<!-- ::end:framework -->
|
|
3165
4096
|
|
|
3166
4097
|
### Hook/logical-based custom UI without resolver
|
|
3167
4098
|
|
|
3168
|
-
|
|
4099
|
+
<!-- ::start:framework -->
|
|
4100
|
+
|
|
4101
|
+
# React
|
|
3169
4102
|
|
|
3170
4103
|
\`\`\`tsx
|
|
3171
4104
|
import { useBlocker } from '@tanstack/react-router'
|
|
@@ -3206,13 +4139,56 @@ function MyComponent() {
|
|
|
3206
4139
|
}
|
|
3207
4140
|
\`\`\`
|
|
3208
4141
|
|
|
3209
|
-
|
|
4142
|
+
# Solid
|
|
4143
|
+
|
|
4144
|
+
\`\`\`tsx
|
|
4145
|
+
import { useBlocker } from '@tanstack/solid-router'
|
|
4146
|
+
|
|
4147
|
+
function MyComponent() {
|
|
4148
|
+
const [formIsDirty, setFormIsDirty] = createSignal(false)
|
|
4149
|
+
|
|
4150
|
+
useBlocker({
|
|
4151
|
+
shouldBlockFn: () => {
|
|
4152
|
+
if (!formIsDirty()) {
|
|
4153
|
+
return false
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
const shouldBlock = new Promise<boolean>((resolve) => {
|
|
4157
|
+
// Using a modal manager of your choice
|
|
4158
|
+
modals.open({
|
|
4159
|
+
title: 'Are you sure you want to leave?',
|
|
4160
|
+
children: (
|
|
4161
|
+
<SaveBlocker
|
|
4162
|
+
confirm={() => {
|
|
4163
|
+
modals.closeAll()
|
|
4164
|
+
resolve(false)
|
|
4165
|
+
}}
|
|
4166
|
+
reject={() => {
|
|
4167
|
+
modals.closeAll()
|
|
4168
|
+
resolve(true)
|
|
4169
|
+
}}
|
|
4170
|
+
/>
|
|
4171
|
+
),
|
|
4172
|
+
onClose: () => resolve(true),
|
|
4173
|
+
})
|
|
4174
|
+
})
|
|
4175
|
+
return shouldBlock
|
|
4176
|
+
},
|
|
4177
|
+
})
|
|
4178
|
+
|
|
4179
|
+
// ...
|
|
4180
|
+
}
|
|
4181
|
+
\`\`\`
|
|
4182
|
+
|
|
4183
|
+
<!-- ::end:framework -->
|
|
3210
4184
|
|
|
3211
4185
|
### Component-based custom UI
|
|
3212
4186
|
|
|
3213
4187
|
Similarly to the hook, the \`Block\` component returns the same state and functions as render props:
|
|
3214
4188
|
|
|
3215
|
-
|
|
4189
|
+
<!-- ::start:framework -->
|
|
4190
|
+
|
|
4191
|
+
# React
|
|
3216
4192
|
|
|
3217
4193
|
\`\`\`tsx
|
|
3218
4194
|
import { Block } from '@tanstack/react-router'
|
|
@@ -3239,7 +4215,34 @@ function MyComponent() {
|
|
|
3239
4215
|
}
|
|
3240
4216
|
\`\`\`
|
|
3241
4217
|
|
|
3242
|
-
|
|
4218
|
+
# Solid
|
|
4219
|
+
|
|
4220
|
+
\`\`\`tsx
|
|
4221
|
+
import { Block } from '@tanstack/solid-router'
|
|
4222
|
+
|
|
4223
|
+
function MyComponent() {
|
|
4224
|
+
const [formIsDirty, setFormIsDirty] = createSignal(false)
|
|
4225
|
+
|
|
4226
|
+
return (
|
|
4227
|
+
<Block shouldBlockFn={() => formIsDirty()} withResolver>
|
|
4228
|
+
{({ status, proceed, reset }) => (
|
|
4229
|
+
<>
|
|
4230
|
+
{/* ... */}
|
|
4231
|
+
{status === 'blocked' && (
|
|
4232
|
+
<div>
|
|
4233
|
+
<p>Are you sure you want to leave?</p>
|
|
4234
|
+
<button onClick={proceed}>Yes</button>
|
|
4235
|
+
<button onClick={reset}>No</button>
|
|
4236
|
+
</div>
|
|
4237
|
+
)}
|
|
4238
|
+
</>
|
|
4239
|
+
)}
|
|
4240
|
+
</Block>
|
|
4241
|
+
)
|
|
4242
|
+
}
|
|
4243
|
+
\`\`\`
|
|
4244
|
+
|
|
4245
|
+
<!-- ::end:framework -->
|
|
3243
4246
|
|
|
3244
4247
|
# Navigation
|
|
3245
4248
|
|
|
@@ -3395,12 +4398,26 @@ export type LinkProps<
|
|
|
3395
4398
|
|
|
3396
4399
|
Let's make a simple static link!
|
|
3397
4400
|
|
|
4401
|
+
<!-- ::start:framework -->
|
|
4402
|
+
|
|
4403
|
+
# React
|
|
4404
|
+
|
|
3398
4405
|
\`\`\`tsx
|
|
3399
4406
|
import { Link } from '@tanstack/react-router'
|
|
3400
4407
|
|
|
3401
4408
|
const link = <Link to="/about">About</Link>
|
|
3402
4409
|
\`\`\`
|
|
3403
4410
|
|
|
4411
|
+
# Solid
|
|
4412
|
+
|
|
4413
|
+
\`\`\`tsx
|
|
4414
|
+
import { Link } from '@tanstack/solid-router'
|
|
4415
|
+
|
|
4416
|
+
const link = <Link to="/about">About</Link>
|
|
4417
|
+
\`\`\`
|
|
4418
|
+
|
|
4419
|
+
<!-- ::end:framework -->
|
|
4420
|
+
|
|
3404
4421
|
### Dynamic Links
|
|
3405
4422
|
|
|
3406
4423
|
Dynamic links are links that have dynamic segments in them. For example, a link to a blog post might look like this:
|
|
@@ -4254,8 +5271,6 @@ export const Route = createFileRoute('/_pathless/route-a')({
|
|
|
4254
5271
|
You can also target the root route by passing the exported \`rootRouteId\` variable to the \`notFound\` function's \`route\` property:
|
|
4255
5272
|
|
|
4256
5273
|
\`\`\`tsx
|
|
4257
|
-
import { rootRouteId } from '@tanstack/react-router'
|
|
4258
|
-
|
|
4259
5274
|
export const Route = createFileRoute('/posts/$postId')({
|
|
4260
5275
|
loader: async ({ params: { postId } }) => {
|
|
4261
5276
|
const post = await getPost(postId)
|
|
@@ -4320,8 +5335,7 @@ The main differences are:
|
|
|
4320
5335
|
|
|
4321
5336
|
To migrate from \`NotFoundRoute\` to \`notFoundComponent\`, you'll just need to make a few changes:
|
|
4322
5337
|
|
|
4323
|
-
\`\`\`tsx
|
|
4324
|
-
// router.tsx
|
|
5338
|
+
\`\`\`tsx title='src/router.tsx'
|
|
4325
5339
|
import { createRouter } from '@tanstack/react-router'
|
|
4326
5340
|
import { routeTree } from './routeTree.gen.'
|
|
4327
5341
|
- import { notFoundRoute } from './notFoundRoute' // [!code --]
|
|
@@ -4361,6 +5375,10 @@ The \`Outlet\` component is used to render the next potentially matching child r
|
|
|
4361
5375
|
|
|
4362
5376
|
A great example is configuring the root route of your application. Let's give our root route a component that renders a title, then an \`<Outlet />\` for our top-level routes to render.
|
|
4363
5377
|
|
|
5378
|
+
<!-- ::start:framework -->
|
|
5379
|
+
|
|
5380
|
+
# React
|
|
5381
|
+
|
|
4364
5382
|
\`\`\`tsx
|
|
4365
5383
|
import { createRootRoute, Outlet } from '@tanstack/react-router'
|
|
4366
5384
|
|
|
@@ -4378,6 +5396,27 @@ function RootComponent() {
|
|
|
4378
5396
|
}
|
|
4379
5397
|
\`\`\`
|
|
4380
5398
|
|
|
5399
|
+
# Solid
|
|
5400
|
+
|
|
5401
|
+
\`\`\`tsx
|
|
5402
|
+
import { createRootRoute, Outlet } from '@tanstack/solid-router'
|
|
5403
|
+
|
|
5404
|
+
export const Route = createRootRoute({
|
|
5405
|
+
component: RootComponent,
|
|
5406
|
+
})
|
|
5407
|
+
|
|
5408
|
+
function RootComponent() {
|
|
5409
|
+
return (
|
|
5410
|
+
<div>
|
|
5411
|
+
<h1>My App</h1>
|
|
5412
|
+
<Outlet /> {/* This is where child routes will render */}
|
|
5413
|
+
</div>
|
|
5414
|
+
)
|
|
5415
|
+
}
|
|
5416
|
+
\`\`\`
|
|
5417
|
+
|
|
5418
|
+
<!-- ::end:framework -->
|
|
5419
|
+
|
|
4381
5420
|
# Parallel Routes
|
|
4382
5421
|
|
|
4383
5422
|
We haven't covered this yet. Stay tuned!
|
|
@@ -4397,9 +5436,11 @@ Because path param routes only match to the next \`/\`, child routes can be crea
|
|
|
4397
5436
|
|
|
4398
5437
|
Let's create a post route file that uses a path param to match the post ID:
|
|
4399
5438
|
|
|
4400
|
-
|
|
5439
|
+
<!-- ::start:framework -->
|
|
4401
5440
|
|
|
4402
|
-
|
|
5441
|
+
# React
|
|
5442
|
+
|
|
5443
|
+
\`\`\`tsx title="src/routes/posts.$postId.tsx"
|
|
4403
5444
|
import { createFileRoute } from '@tanstack/react-router'
|
|
4404
5445
|
|
|
4405
5446
|
export const Route = createFileRoute('/posts/$postId')({
|
|
@@ -4409,6 +5450,20 @@ export const Route = createFileRoute('/posts/$postId')({
|
|
|
4409
5450
|
})
|
|
4410
5451
|
\`\`\`
|
|
4411
5452
|
|
|
5453
|
+
# Solid
|
|
5454
|
+
|
|
5455
|
+
\`\`\`tsx title="src/routes/posts.$postId.tsx"
|
|
5456
|
+
import { createFileRoute } from '@tanstack/solid-router'
|
|
5457
|
+
|
|
5458
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
5459
|
+
loader: async ({ params }) => {
|
|
5460
|
+
return fetchPost(params.postId)
|
|
5461
|
+
},
|
|
5462
|
+
})
|
|
5463
|
+
\`\`\`
|
|
5464
|
+
|
|
5465
|
+
<!-- ::end:framework -->
|
|
5466
|
+
|
|
4412
5467
|
## Path Params can be used by child routes
|
|
4413
5468
|
|
|
4414
5469
|
Once a path param has been parsed, it is available to all child routes. This means that if we define a child route to our \`postRoute\`, we can use the \`postId\` variable from the URL in the child route's path!
|
|
@@ -4417,7 +5472,7 @@ Once a path param has been parsed, it is available to all child routes. This mea
|
|
|
4417
5472
|
|
|
4418
5473
|
Path params are passed to the loader as a \`params\` object. The keys of this object are the names of the path params, and the values are the values that were parsed out of the actual URL path. For example, if we were to visit the \`/blog/123\` URL, the \`params\` object would be \`{ postId: '123' }\`:
|
|
4419
5474
|
|
|
4420
|
-
\`\`\`tsx
|
|
5475
|
+
\`\`\`tsx title="src/routes/posts.$postId.tsx"
|
|
4421
5476
|
export const Route = createFileRoute('/posts/$postId')({
|
|
4422
5477
|
loader: async ({ params }) => {
|
|
4423
5478
|
return fetchPost(params.postId)
|
|
@@ -4427,7 +5482,7 @@ export const Route = createFileRoute('/posts/$postId')({
|
|
|
4427
5482
|
|
|
4428
5483
|
The \`params\` object is also passed to the \`beforeLoad\` option:
|
|
4429
5484
|
|
|
4430
|
-
\`\`\`tsx
|
|
5485
|
+
\`\`\`tsx title="src/routes/posts.$postId.tsx"
|
|
4431
5486
|
export const Route = createFileRoute('/posts/$postId')({
|
|
4432
5487
|
beforeLoad: async ({ params }) => {
|
|
4433
5488
|
// do something with params.postId
|
|
@@ -4439,7 +5494,11 @@ export const Route = createFileRoute('/posts/$postId')({
|
|
|
4439
5494
|
|
|
4440
5495
|
If we add a component to our \`postRoute\`, we can access the \`postId\` variable from the URL by using the route's \`useParams\` hook:
|
|
4441
5496
|
|
|
4442
|
-
|
|
5497
|
+
<!-- ::start:framework -->
|
|
5498
|
+
|
|
5499
|
+
# React
|
|
5500
|
+
|
|
5501
|
+
\`\`\`tsx title="src/routes/posts.$postId.tsx"
|
|
4443
5502
|
export const Route = createFileRoute('/posts/$postId')({
|
|
4444
5503
|
component: PostComponent,
|
|
4445
5504
|
})
|
|
@@ -4450,19 +5509,49 @@ function PostComponent() {
|
|
|
4450
5509
|
}
|
|
4451
5510
|
\`\`\`
|
|
4452
5511
|
|
|
5512
|
+
# Solid
|
|
5513
|
+
|
|
5514
|
+
\`\`\`tsx title="src/routes/posts.$postId.tsx"
|
|
5515
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
5516
|
+
component: PostComponent,
|
|
5517
|
+
})
|
|
5518
|
+
|
|
5519
|
+
function PostComponent() {
|
|
5520
|
+
const params = Route.useParams()
|
|
5521
|
+
return <div>Post {params().postId}</div>
|
|
5522
|
+
}
|
|
5523
|
+
\`\`\`
|
|
5524
|
+
|
|
5525
|
+
<!-- ::end:framework -->
|
|
5526
|
+
|
|
4453
5527
|
> 🧠 Quick tip: If your component is code-split, you can use the [getRouteApi function](./code-splitting.md#manually-accessing-route-apis-in-other-files-with-the-getrouteapi-helper) to avoid having to import the \`Route\` configuration to get access to the typed \`useParams()\` hook.
|
|
4454
5528
|
|
|
4455
5529
|
## Path Params outside of Routes
|
|
4456
5530
|
|
|
4457
5531
|
You can also use the globally exported \`useParams\` hook to access any parsed path params from any component in your app. You'll need to pass the \`strict: false\` option to \`useParams\`, denoting that you want to access the params from an ambiguous location:
|
|
4458
5532
|
|
|
4459
|
-
|
|
5533
|
+
<!-- ::start:framework -->
|
|
5534
|
+
|
|
5535
|
+
# React
|
|
5536
|
+
|
|
5537
|
+
\`\`\`tsx title="src/components/PostComponent.tsx"
|
|
4460
5538
|
function PostComponent() {
|
|
4461
5539
|
const { postId } = useParams({ strict: false })
|
|
4462
5540
|
return <div>Post {postId}</div>
|
|
4463
5541
|
}
|
|
4464
5542
|
\`\`\`
|
|
4465
5543
|
|
|
5544
|
+
# Solid
|
|
5545
|
+
|
|
5546
|
+
\`\`\`tsx title="src/components/PostComponent.tsx"
|
|
5547
|
+
function PostComponent() {
|
|
5548
|
+
const params = useParams({ strict: false })
|
|
5549
|
+
return <div>Post {params().postId}</div>
|
|
5550
|
+
}
|
|
5551
|
+
\`\`\`
|
|
5552
|
+
|
|
5553
|
+
<!-- ::end:framework -->
|
|
5554
|
+
|
|
4466
5555
|
## Navigating with Path Params
|
|
4467
5556
|
|
|
4468
5557
|
When navigating to a route with path params, TypeScript will require you to pass the params either as an object or as a function that returns an object of params.
|
|
@@ -4503,8 +5592,11 @@ When using either prefixes or suffixes, you can define them by wrapping the path
|
|
|
4503
5592
|
|
|
4504
5593
|
Prefixes are defined by placing the prefix text outside the curly braces before the variable name. For example, if you want to match a URL that starts with \`post-\` followed by a post ID, you can define it like this:
|
|
4505
5594
|
|
|
4506
|
-
|
|
4507
|
-
|
|
5595
|
+
<!-- ::start:framework -->
|
|
5596
|
+
|
|
5597
|
+
# React
|
|
5598
|
+
|
|
5599
|
+
\`\`\`tsx title="src/routes/posts/post-{$postId}.tsx"
|
|
4508
5600
|
export const Route = createFileRoute('/posts/post-{$postId}')({
|
|
4509
5601
|
component: PostComponent,
|
|
4510
5602
|
})
|
|
@@ -4516,10 +5608,29 @@ function PostComponent() {
|
|
|
4516
5608
|
}
|
|
4517
5609
|
\`\`\`
|
|
4518
5610
|
|
|
5611
|
+
# Solid
|
|
5612
|
+
|
|
5613
|
+
\`\`\`tsx title="src/routes/posts/post-{$postId}.tsx"
|
|
5614
|
+
export const Route = createFileRoute('/posts/post-{$postId}')({
|
|
5615
|
+
component: PostComponent,
|
|
5616
|
+
})
|
|
5617
|
+
|
|
5618
|
+
function PostComponent() {
|
|
5619
|
+
const params = Route.useParams()
|
|
5620
|
+
// postId will be the value after 'post-'
|
|
5621
|
+
return <div>Post ID: {params().postId}</div>
|
|
5622
|
+
}
|
|
5623
|
+
\`\`\`
|
|
5624
|
+
|
|
5625
|
+
<!-- ::end:framework -->
|
|
5626
|
+
|
|
4519
5627
|
You can even combines prefixes with wildcard routes to create more complex patterns:
|
|
4520
5628
|
|
|
4521
|
-
|
|
4522
|
-
|
|
5629
|
+
<!-- ::start:framework -->
|
|
5630
|
+
|
|
5631
|
+
# React
|
|
5632
|
+
|
|
5633
|
+
\`\`\`tsx title="src/routes/on-disk/storage-{$postId}/$.tsx"
|
|
4523
5634
|
export const Route = createFileRoute('/on-disk/storage-{$postId}/$')({
|
|
4524
5635
|
component: StorageComponent,
|
|
4525
5636
|
})
|
|
@@ -4532,12 +5643,32 @@ function StorageComponent() {
|
|
|
4532
5643
|
}
|
|
4533
5644
|
\`\`\`
|
|
4534
5645
|
|
|
5646
|
+
# Solid
|
|
5647
|
+
|
|
5648
|
+
\`\`\`tsx title="src/routes/on-disk/storage-{$postId}/$.tsx"
|
|
5649
|
+
export const Route = createFileRoute('/on-disk/storage-{$postId}/$')({
|
|
5650
|
+
component: StorageComponent,
|
|
5651
|
+
})
|
|
5652
|
+
|
|
5653
|
+
function StorageComponent() {
|
|
5654
|
+
const params = Route.useParams()
|
|
5655
|
+
// _splat, will be value after 'storage-'
|
|
5656
|
+
// i.e. my-drive/documents/foo.txt
|
|
5657
|
+
return <div>Storage Location: /{params()._splat}</div>
|
|
5658
|
+
}
|
|
5659
|
+
\`\`\`
|
|
5660
|
+
|
|
5661
|
+
<!-- ::end:framework -->
|
|
5662
|
+
|
|
4535
5663
|
### Defining Suffixes
|
|
4536
5664
|
|
|
4537
5665
|
Suffixes are defined by placing the suffix text outside the curly braces after the variable name. For example, if you want to match a URL a filename that ends with \`txt\`, you can define it like this:
|
|
4538
5666
|
|
|
4539
|
-
|
|
4540
|
-
|
|
5667
|
+
<!-- ::start:framework -->
|
|
5668
|
+
|
|
5669
|
+
# React
|
|
5670
|
+
|
|
5671
|
+
\`\`\`tsx title="src/routes/files/{$fileName}[.]txt.tsx"
|
|
4541
5672
|
export const Route = createFileRoute('/files/{$fileName}.txt')({
|
|
4542
5673
|
component: FileComponent,
|
|
4543
5674
|
})
|
|
@@ -4549,10 +5680,29 @@ function FileComponent() {
|
|
|
4549
5680
|
}
|
|
4550
5681
|
\`\`\`
|
|
4551
5682
|
|
|
5683
|
+
# Solid
|
|
5684
|
+
|
|
5685
|
+
\`\`\`tsx title="src/routes/files/{$fileName}[.]txt.tsx"
|
|
5686
|
+
export const Route = createFileRoute('/files/{$fileName}.txt')({
|
|
5687
|
+
component: FileComponent,
|
|
5688
|
+
})
|
|
5689
|
+
|
|
5690
|
+
function FileComponent() {
|
|
5691
|
+
const params = Route.useParams()
|
|
5692
|
+
// fileName will be the value before 'txt'
|
|
5693
|
+
return <div>File Name: {params().fileName}</div>
|
|
5694
|
+
}
|
|
5695
|
+
\`\`\`
|
|
5696
|
+
|
|
5697
|
+
<!-- ::end:framework -->
|
|
5698
|
+
|
|
4552
5699
|
You can also combine suffixes with wildcards for more complex routing patterns:
|
|
4553
5700
|
|
|
4554
|
-
|
|
4555
|
-
|
|
5701
|
+
<!-- ::start:framework -->
|
|
5702
|
+
|
|
5703
|
+
# React
|
|
5704
|
+
|
|
5705
|
+
\`\`\`tsx title="src/routes/files/{$}[.]txt.tsx"
|
|
4556
5706
|
export const Route = createFileRoute('/files/{$fileName}[.]txt')({
|
|
4557
5707
|
component: FileComponent,
|
|
4558
5708
|
})
|
|
@@ -4564,12 +5714,31 @@ function FileComponent() {
|
|
|
4564
5714
|
}
|
|
4565
5715
|
\`\`\`
|
|
4566
5716
|
|
|
5717
|
+
# Solid
|
|
5718
|
+
|
|
5719
|
+
\`\`\`tsx title="src/routes/files/{$}[.]txt.tsx"
|
|
5720
|
+
export const Route = createFileRoute('/files/{$fileName}[.]txt')({
|
|
5721
|
+
component: FileComponent,
|
|
5722
|
+
})
|
|
5723
|
+
|
|
5724
|
+
function FileComponent() {
|
|
5725
|
+
const params = Route.useParams()
|
|
5726
|
+
// _splat will be the value before '.txt'
|
|
5727
|
+
return <div>File Splat: {params()._splat}</div>
|
|
5728
|
+
}
|
|
5729
|
+
\`\`\`
|
|
5730
|
+
|
|
5731
|
+
<!-- ::end:framework -->
|
|
5732
|
+
|
|
4567
5733
|
### Combining Prefixes and Suffixes
|
|
4568
5734
|
|
|
4569
5735
|
You can combine both prefixes and suffixes to create very specific routing patterns. For example, if you want to match a URL that starts with \`user-\` and ends with \`.json\`, you can define it like this:
|
|
4570
5736
|
|
|
4571
|
-
|
|
4572
|
-
|
|
5737
|
+
<!-- ::start:framework -->
|
|
5738
|
+
|
|
5739
|
+
# React
|
|
5740
|
+
|
|
5741
|
+
\`\`\`tsx title="src/routes/users/user-{$userId}.json"
|
|
4573
5742
|
export const Route = createFileRoute('/users/user-{$userId}.json')({
|
|
4574
5743
|
component: UserComponent,
|
|
4575
5744
|
})
|
|
@@ -4581,6 +5750,22 @@ function UserComponent() {
|
|
|
4581
5750
|
}
|
|
4582
5751
|
\`\`\`
|
|
4583
5752
|
|
|
5753
|
+
# Solid
|
|
5754
|
+
|
|
5755
|
+
\`\`\`tsx title="src/routes/users/user-{$userId}.json"
|
|
5756
|
+
export const Route = createFileRoute('/users/user-{$userId}.json')({
|
|
5757
|
+
component: UserComponent,
|
|
5758
|
+
})
|
|
5759
|
+
|
|
5760
|
+
function UserComponent() {
|
|
5761
|
+
const params = Route.useParams()
|
|
5762
|
+
// userId will be the value between 'user-' and '.json'
|
|
5763
|
+
return <div>User ID: {params().userId}</div>
|
|
5764
|
+
}
|
|
5765
|
+
\`\`\`
|
|
5766
|
+
|
|
5767
|
+
<!-- ::end:framework -->
|
|
5768
|
+
|
|
4584
5769
|
Similar to the previous examples, you can also use wildcards with prefixes and suffixes. Go wild!
|
|
4585
5770
|
|
|
4586
5771
|
## Optional Path Parameters
|
|
@@ -4625,7 +5810,11 @@ When an optional parameter is not present in the URL, its value will be \`undefi
|
|
|
4625
5810
|
|
|
4626
5811
|
Optional parameters work exactly like regular parameters in your components, but their values may be \`undefined\`:
|
|
4627
5812
|
|
|
4628
|
-
|
|
5813
|
+
<!-- ::start:framework -->
|
|
5814
|
+
|
|
5815
|
+
# React
|
|
5816
|
+
|
|
5817
|
+
\`\`\`tsx title="src/routes/posts/{-$category}.tsx"
|
|
4629
5818
|
function PostsComponent() {
|
|
4630
5819
|
const { category } = Route.useParams()
|
|
4631
5820
|
|
|
@@ -4633,13 +5822,29 @@ function PostsComponent() {
|
|
|
4633
5822
|
}
|
|
4634
5823
|
\`\`\`
|
|
4635
5824
|
|
|
4636
|
-
|
|
5825
|
+
# Solid
|
|
4637
5826
|
|
|
4638
|
-
|
|
5827
|
+
\`\`\`tsx title="src/routes/posts/{-$category}.tsx"
|
|
5828
|
+
function PostsComponent() {
|
|
5829
|
+
const params = Route.useParams()
|
|
4639
5830
|
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
5831
|
+
return (
|
|
5832
|
+
<div>
|
|
5833
|
+
{params().category ? \`Posts in \${params().category}\` : 'All Posts'}
|
|
5834
|
+
</div>
|
|
5835
|
+
)
|
|
5836
|
+
}
|
|
5837
|
+
\`\`\`
|
|
5838
|
+
|
|
5839
|
+
<!-- ::end:framework -->
|
|
5840
|
+
|
|
5841
|
+
### Optional Parameters in Loaders
|
|
5842
|
+
|
|
5843
|
+
Optional parameters are available in loaders and may be \`undefined\`:
|
|
5844
|
+
|
|
5845
|
+
\`\`\`tsx
|
|
5846
|
+
export const Route = createFileRoute('/posts/{-$category}')({
|
|
5847
|
+
loader: async ({ params }) => {
|
|
4643
5848
|
// params.category might be undefined
|
|
4644
5849
|
return fetchPosts({ category: params.category })
|
|
4645
5850
|
},
|
|
@@ -4667,8 +5872,12 @@ export const Route = createFileRoute('/posts/{-$category}')({
|
|
|
4667
5872
|
|
|
4668
5873
|
Optional parameters support prefix and suffix patterns:
|
|
4669
5874
|
|
|
4670
|
-
|
|
4671
|
-
|
|
5875
|
+
<!-- ::start:framework -->
|
|
5876
|
+
|
|
5877
|
+
# React
|
|
5878
|
+
|
|
5879
|
+
\`\`\`tsx title="src/routes/files/prefix{-$name}.txt"
|
|
5880
|
+
// Route: /files/prefix{-$name}.txt
|
|
4672
5881
|
// Matches: /files/prefix.txt and /files/prefixdocument.txt
|
|
4673
5882
|
export const Route = createFileRoute('/files/prefix{-$name}.txt')({
|
|
4674
5883
|
component: FileComponent,
|
|
@@ -4680,11 +5889,32 @@ function FileComponent() {
|
|
|
4680
5889
|
}
|
|
4681
5890
|
\`\`\`
|
|
4682
5891
|
|
|
5892
|
+
# Solid
|
|
5893
|
+
|
|
5894
|
+
\`\`\`tsx title="src/routes/files/prefix{-$name}.txt"
|
|
5895
|
+
// Route: /files/prefix{-$name}.txt
|
|
5896
|
+
// Matches: /files/prefix.txt and /files/prefixdocument.txt
|
|
5897
|
+
export const Route = createFileRoute('/files/prefix{-$name}.txt')({
|
|
5898
|
+
component: FileComponent,
|
|
5899
|
+
})
|
|
5900
|
+
|
|
5901
|
+
function FileComponent() {
|
|
5902
|
+
const params = Route.useParams()
|
|
5903
|
+
return <div>File: {params().name || 'default'}</div>
|
|
5904
|
+
}
|
|
5905
|
+
\`\`\`
|
|
5906
|
+
|
|
5907
|
+
<!-- ::end:framework -->
|
|
5908
|
+
|
|
4683
5909
|
#### All Optional Parameters
|
|
4684
5910
|
|
|
4685
5911
|
You can create routes where all parameters are optional:
|
|
4686
5912
|
|
|
4687
|
-
|
|
5913
|
+
<!-- ::start:framework -->
|
|
5914
|
+
|
|
5915
|
+
# React
|
|
5916
|
+
|
|
5917
|
+
\`\`\`tsx title="src/routes/{-$year}/{-$month}/{-$day}.tsx"
|
|
4688
5918
|
// Route: /{-$year}/{-$month}/{-$day}
|
|
4689
5919
|
// Matches: /, /2023, /2023/12, /2023/12/25
|
|
4690
5920
|
export const Route = createFileRoute('/{-$year}/{-$month}/{-$day}')({
|
|
@@ -4711,11 +5941,46 @@ function DateComponent() {
|
|
|
4711
5941
|
}
|
|
4712
5942
|
\`\`\`
|
|
4713
5943
|
|
|
5944
|
+
# Solid
|
|
5945
|
+
|
|
5946
|
+
\`\`\`tsx title="src/routes/{-$year}/{-$month}/{-$day}.tsx"
|
|
5947
|
+
// Route: /{-$year}/{-$month}/{-$day}
|
|
5948
|
+
// Matches: /, /2023, /2023/12, /2023/12/25
|
|
5949
|
+
export const Route = createFileRoute('/{-$year}/{-$month}/{-$day}')({
|
|
5950
|
+
component: DateComponent,
|
|
5951
|
+
})
|
|
5952
|
+
|
|
5953
|
+
function DateComponent() {
|
|
5954
|
+
const params = Route.useParams()
|
|
5955
|
+
|
|
5956
|
+
if (!params().year) return <div>Select a year</div>
|
|
5957
|
+
if (!params().month) return <div>Year: {params().year}</div>
|
|
5958
|
+
if (!params().day)
|
|
5959
|
+
return (
|
|
5960
|
+
<div>
|
|
5961
|
+
Month: {params().year}/{params().month}
|
|
5962
|
+
</div>
|
|
5963
|
+
)
|
|
5964
|
+
|
|
5965
|
+
return (
|
|
5966
|
+
<div>
|
|
5967
|
+
Date: {params().year}/{params().month}/{params().day}
|
|
5968
|
+
</div>
|
|
5969
|
+
)
|
|
5970
|
+
}
|
|
5971
|
+
\`\`\`
|
|
5972
|
+
|
|
5973
|
+
<!-- ::end:framework -->
|
|
5974
|
+
|
|
4714
5975
|
#### Optional Parameters with Wildcards
|
|
4715
5976
|
|
|
4716
5977
|
Optional parameters can be combined with wildcards for complex routing patterns:
|
|
4717
5978
|
|
|
4718
|
-
|
|
5979
|
+
<!-- ::start:framework -->
|
|
5980
|
+
|
|
5981
|
+
# React
|
|
5982
|
+
|
|
5983
|
+
\`\`\`tsx title="src/routes/docs/{-$version}/$.tsx"
|
|
4719
5984
|
// Route: /docs/{-$version}/$
|
|
4720
5985
|
// Matches: /docs/extra/path, /docs/v2/extra/path
|
|
4721
5986
|
export const Route = createFileRoute('/docs/{-$version}/$')({
|
|
@@ -4735,6 +6000,29 @@ function DocsComponent() {
|
|
|
4735
6000
|
}
|
|
4736
6001
|
\`\`\`
|
|
4737
6002
|
|
|
6003
|
+
# Solid
|
|
6004
|
+
|
|
6005
|
+
\`\`\`tsx title="src/routes/docs/{-$version}/$.tsx"
|
|
6006
|
+
// Route: /docs/{-$version}/$
|
|
6007
|
+
// Matches: /docs/extra/path, /docs/v2/extra/path
|
|
6008
|
+
export const Route = createFileRoute('/docs/{-$version}/$')({
|
|
6009
|
+
component: DocsComponent,
|
|
6010
|
+
})
|
|
6011
|
+
|
|
6012
|
+
function DocsComponent() {
|
|
6013
|
+
const params = Route.useParams()
|
|
6014
|
+
|
|
6015
|
+
return (
|
|
6016
|
+
<div>
|
|
6017
|
+
Version: {params().version || 'latest'}
|
|
6018
|
+
Path: {params()._splat}
|
|
6019
|
+
</div>
|
|
6020
|
+
)
|
|
6021
|
+
}
|
|
6022
|
+
\`\`\`
|
|
6023
|
+
|
|
6024
|
+
<!-- ::end:framework -->
|
|
6025
|
+
|
|
4738
6026
|
### Navigating with Optional Parameters
|
|
4739
6027
|
|
|
4740
6028
|
When navigating to routes with optional parameters, you have fine-grained control over which parameters to include:
|
|
@@ -4769,7 +6057,11 @@ function Navigation() {
|
|
|
4769
6057
|
|
|
4770
6058
|
TypeScript provides full type safety for optional parameters:
|
|
4771
6059
|
|
|
4772
|
-
|
|
6060
|
+
<!-- ::start:framework -->
|
|
6061
|
+
|
|
6062
|
+
# React
|
|
6063
|
+
|
|
6064
|
+
\`\`\`tsx title="src/routes/posts/{-$category}.tsx"
|
|
4773
6065
|
function PostsComponent() {
|
|
4774
6066
|
// TypeScript knows category might be undefined
|
|
4775
6067
|
const { category } = Route.useParams() // category: string | undefined
|
|
@@ -4796,6 +6088,37 @@ function PostsComponent() {
|
|
|
4796
6088
|
</Link>
|
|
4797
6089
|
\`\`\`
|
|
4798
6090
|
|
|
6091
|
+
# Solid
|
|
6092
|
+
|
|
6093
|
+
\`\`\`tsx title="src/routes/posts/{-$category}.tsx"
|
|
6094
|
+
function PostsComponent() {
|
|
6095
|
+
// TypeScript knows category might be undefined
|
|
6096
|
+
const params = Route.useParams() // category: string | undefined
|
|
6097
|
+
|
|
6098
|
+
// Safe navigation
|
|
6099
|
+
const categoryUpper = params().category?.toUpperCase()
|
|
6100
|
+
|
|
6101
|
+
return <div>{categoryUpper || 'All Categories'}</div>
|
|
6102
|
+
}
|
|
6103
|
+
|
|
6104
|
+
// Navigation is type-safe and flexible
|
|
6105
|
+
<Link
|
|
6106
|
+
to="/posts/{-$category}"
|
|
6107
|
+
params={{ category: 'tech' }} // ✅ Valid - string
|
|
6108
|
+
>
|
|
6109
|
+
Tech Posts
|
|
6110
|
+
</Link>
|
|
6111
|
+
|
|
6112
|
+
<Link
|
|
6113
|
+
to="/posts/{-$category}"
|
|
6114
|
+
params={{ category: 123 }} // ✅ Valid - number (auto-stringified)
|
|
6115
|
+
>
|
|
6116
|
+
Category 123
|
|
6117
|
+
</Link>
|
|
6118
|
+
\`\`\`
|
|
6119
|
+
|
|
6120
|
+
<!-- ::end:framework -->
|
|
6121
|
+
|
|
4799
6122
|
## Internationalization (i18n) with Optional Path Parameters
|
|
4800
6123
|
|
|
4801
6124
|
Optional path parameters are excellent for implementing internationalization (i18n) routing patterns. You can use prefix patterns to handle multiple languages while maintaining clean, SEO-friendly URLs.
|
|
@@ -4804,7 +6127,11 @@ Optional path parameters are excellent for implementing internationalization (i1
|
|
|
4804
6127
|
|
|
4805
6128
|
Use optional language prefixes to support URLs like \`/en/about\`, \`/fr/about\`, or just \`/about\` (default language):
|
|
4806
6129
|
|
|
4807
|
-
|
|
6130
|
+
<!-- ::start:framework -->
|
|
6131
|
+
|
|
6132
|
+
# React
|
|
6133
|
+
|
|
6134
|
+
\`\`\`tsx title="src/routes/{-$locale}/about.tsx"
|
|
4808
6135
|
// Route: /{-$locale}/about
|
|
4809
6136
|
export const Route = createFileRoute('/{-$locale}/about')({
|
|
4810
6137
|
component: AboutComponent,
|
|
@@ -4835,6 +6162,41 @@ function AboutComponent() {
|
|
|
4835
6162
|
}
|
|
4836
6163
|
\`\`\`
|
|
4837
6164
|
|
|
6165
|
+
# Solid
|
|
6166
|
+
|
|
6167
|
+
\`\`\`tsx title="src/routes/{-$locale}/about.tsx"
|
|
6168
|
+
// Route: /{-$locale}/about
|
|
6169
|
+
export const Route = createFileRoute('/{-$locale}/about')({
|
|
6170
|
+
component: AboutComponent,
|
|
6171
|
+
})
|
|
6172
|
+
|
|
6173
|
+
function AboutComponent() {
|
|
6174
|
+
const params = Route.useParams()
|
|
6175
|
+
const currentLocale = params().locale || 'en' // Default to English
|
|
6176
|
+
|
|
6177
|
+
const content = {
|
|
6178
|
+
en: { title: 'About Us', description: 'Learn more about our company.' },
|
|
6179
|
+
fr: {
|
|
6180
|
+
title: 'À Propos',
|
|
6181
|
+
description: 'En savoir plus sur notre entreprise.',
|
|
6182
|
+
},
|
|
6183
|
+
es: {
|
|
6184
|
+
title: 'Acerca de',
|
|
6185
|
+
description: 'Conoce más sobre nuestra empresa.',
|
|
6186
|
+
},
|
|
6187
|
+
}
|
|
6188
|
+
|
|
6189
|
+
return (
|
|
6190
|
+
<div>
|
|
6191
|
+
<h1>{content[currentLocale]?.title}</h1>
|
|
6192
|
+
<p>{content[currentLocale]?.description}</p>
|
|
6193
|
+
</div>
|
|
6194
|
+
)
|
|
6195
|
+
}
|
|
6196
|
+
\`\`\`
|
|
6197
|
+
|
|
6198
|
+
<!-- ::end:framework -->
|
|
6199
|
+
|
|
4838
6200
|
This pattern matches:
|
|
4839
6201
|
|
|
4840
6202
|
- \`/about\` (default locale)
|
|
@@ -4846,7 +6208,11 @@ This pattern matches:
|
|
|
4846
6208
|
|
|
4847
6209
|
Combine optional parameters for more sophisticated i18n routing:
|
|
4848
6210
|
|
|
4849
|
-
|
|
6211
|
+
<!-- ::start:framework -->
|
|
6212
|
+
|
|
6213
|
+
# React
|
|
6214
|
+
|
|
6215
|
+
\`\`\`tsx title="src/routes/{-$locale}/blog/{-$category}/$slug.tsx"
|
|
4850
6216
|
// Route: /{-$locale}/blog/{-$category}/$slug
|
|
4851
6217
|
export const Route = createFileRoute('/{-$locale}/blog/{-$category}/$slug')({
|
|
4852
6218
|
beforeLoad: async ({ params }) => {
|
|
@@ -4886,6 +6252,51 @@ function BlogPostComponent() {
|
|
|
4886
6252
|
}
|
|
4887
6253
|
\`\`\`
|
|
4888
6254
|
|
|
6255
|
+
# Solid
|
|
6256
|
+
|
|
6257
|
+
\`\`\`tsx title="src/routes/{-$locale}/blog/{-$category}/$slug.tsx"
|
|
6258
|
+
// Route: /{-$locale}/blog/{-$category}/$slug
|
|
6259
|
+
export const Route = createFileRoute('/{-$locale}/blog/{-$category}/$slug')({
|
|
6260
|
+
beforeLoad: async ({ params }) => {
|
|
6261
|
+
const locale = params.locale || 'en'
|
|
6262
|
+
const category = params.category
|
|
6263
|
+
|
|
6264
|
+
// Validate locale and category
|
|
6265
|
+
const validLocales = ['en', 'fr', 'es', 'de']
|
|
6266
|
+
if (locale && !validLocales.includes(locale)) {
|
|
6267
|
+
throw new Error('Invalid locale')
|
|
6268
|
+
}
|
|
6269
|
+
|
|
6270
|
+
return { locale, category }
|
|
6271
|
+
},
|
|
6272
|
+
loader: async ({ params, context }) => {
|
|
6273
|
+
const { locale } = context
|
|
6274
|
+
const { slug, category } = params
|
|
6275
|
+
|
|
6276
|
+
return fetchBlogPost({ slug, category, locale })
|
|
6277
|
+
},
|
|
6278
|
+
component: BlogPostComponent,
|
|
6279
|
+
})
|
|
6280
|
+
|
|
6281
|
+
function BlogPostComponent() {
|
|
6282
|
+
const params = Route.useParams()
|
|
6283
|
+
const data = Route.useLoaderData()
|
|
6284
|
+
|
|
6285
|
+
return (
|
|
6286
|
+
<article>
|
|
6287
|
+
<h1>{data.title}</h1>
|
|
6288
|
+
<p>
|
|
6289
|
+
Category: {params().category || 'All'} | Language:{' '}
|
|
6290
|
+
{params().locale || 'en'}
|
|
6291
|
+
</p>
|
|
6292
|
+
<div>{data.content}</div>
|
|
6293
|
+
</article>
|
|
6294
|
+
)
|
|
6295
|
+
}
|
|
6296
|
+
\`\`\`
|
|
6297
|
+
|
|
6298
|
+
<!-- ::end:framework -->
|
|
6299
|
+
|
|
4889
6300
|
This supports URLs like:
|
|
4890
6301
|
|
|
4891
6302
|
- \`/blog/tech/my-post\` (default locale, tech category)
|
|
@@ -4897,7 +6308,11 @@ This supports URLs like:
|
|
|
4897
6308
|
|
|
4898
6309
|
Create language switchers using optional i18n parameters with function-style params:
|
|
4899
6310
|
|
|
4900
|
-
|
|
6311
|
+
<!-- ::start:framework -->
|
|
6312
|
+
|
|
6313
|
+
# React
|
|
6314
|
+
|
|
6315
|
+
\`\`\`tsx title="src/components/LanguageSwitcher.tsx"
|
|
4901
6316
|
function LanguageSwitcher() {
|
|
4902
6317
|
const currentParams = useParams({ strict: false })
|
|
4903
6318
|
|
|
@@ -4927,8 +6342,45 @@ function LanguageSwitcher() {
|
|
|
4927
6342
|
}
|
|
4928
6343
|
\`\`\`
|
|
4929
6344
|
|
|
6345
|
+
# Solid
|
|
6346
|
+
|
|
6347
|
+
\`\`\`tsx title="src/components/LanguageSwitcher.tsx"
|
|
6348
|
+
function LanguageSwitcher() {
|
|
6349
|
+
const currentParams = useParams({ strict: false })
|
|
6350
|
+
|
|
6351
|
+
const languages = [
|
|
6352
|
+
{ code: 'en', name: 'English' },
|
|
6353
|
+
{ code: 'fr', name: 'Français' },
|
|
6354
|
+
{ code: 'es', name: 'Español' },
|
|
6355
|
+
]
|
|
6356
|
+
|
|
6357
|
+
return (
|
|
6358
|
+
<div class="language-switcher">
|
|
6359
|
+
{languages.map(({ code, name }) => (
|
|
6360
|
+
<Link
|
|
6361
|
+
to="/{-$locale}/blog/{-$category}/$slug"
|
|
6362
|
+
params={(prev) => ({
|
|
6363
|
+
...prev,
|
|
6364
|
+
locale: code === 'en' ? undefined : code, // Remove 'en' for clean URLs
|
|
6365
|
+
})}
|
|
6366
|
+
class={currentParams().locale === code ? 'active' : ''}
|
|
6367
|
+
>
|
|
6368
|
+
{name}
|
|
6369
|
+
</Link>
|
|
6370
|
+
))}
|
|
6371
|
+
</div>
|
|
6372
|
+
)
|
|
6373
|
+
}
|
|
6374
|
+
\`\`\`
|
|
6375
|
+
|
|
6376
|
+
<!-- ::end:framework -->
|
|
6377
|
+
|
|
4930
6378
|
You can also create more sophisticated language switching logic:
|
|
4931
6379
|
|
|
6380
|
+
<!-- ::start:framework -->
|
|
6381
|
+
|
|
6382
|
+
# React
|
|
6383
|
+
|
|
4932
6384
|
\`\`\`tsx
|
|
4933
6385
|
function AdvancedLanguageSwitcher() {
|
|
4934
6386
|
const currentParams = useParams({ strict: false })
|
|
@@ -4976,10 +6428,65 @@ function AdvancedLanguageSwitcher() {
|
|
|
4976
6428
|
}
|
|
4977
6429
|
\`\`\`
|
|
4978
6430
|
|
|
6431
|
+
# Solid
|
|
6432
|
+
|
|
6433
|
+
\`\`\`tsx
|
|
6434
|
+
function AdvancedLanguageSwitcher() {
|
|
6435
|
+
const currentParams = useParams({ strict: false })
|
|
6436
|
+
|
|
6437
|
+
const handleLanguageChange = (newLocale: string) => {
|
|
6438
|
+
return (prev: any) => {
|
|
6439
|
+
// Preserve all existing params but update locale
|
|
6440
|
+
const updatedParams = { ...prev }
|
|
6441
|
+
|
|
6442
|
+
if (newLocale === 'en') {
|
|
6443
|
+
// Remove locale for clean English URLs
|
|
6444
|
+
delete updatedParams.locale
|
|
6445
|
+
} else {
|
|
6446
|
+
updatedParams.locale = newLocale
|
|
6447
|
+
}
|
|
6448
|
+
|
|
6449
|
+
return updatedParams
|
|
6450
|
+
}
|
|
6451
|
+
}
|
|
6452
|
+
|
|
6453
|
+
return (
|
|
6454
|
+
<div class="language-switcher">
|
|
6455
|
+
<Link
|
|
6456
|
+
to="/{-$locale}/blog/{-$category}/$slug"
|
|
6457
|
+
params={handleLanguageChange('fr')}
|
|
6458
|
+
>
|
|
6459
|
+
Français
|
|
6460
|
+
</Link>
|
|
6461
|
+
|
|
6462
|
+
<Link
|
|
6463
|
+
to="/{-$locale}/blog/{-$category}/$slug"
|
|
6464
|
+
params={handleLanguageChange('es')}
|
|
6465
|
+
>
|
|
6466
|
+
Español
|
|
6467
|
+
</Link>
|
|
6468
|
+
|
|
6469
|
+
<Link
|
|
6470
|
+
to="/{-$locale}/blog/{-$category}/$slug"
|
|
6471
|
+
params={handleLanguageChange('en')}
|
|
6472
|
+
>
|
|
6473
|
+
English
|
|
6474
|
+
</Link>
|
|
6475
|
+
</div>
|
|
6476
|
+
)
|
|
6477
|
+
}
|
|
6478
|
+
\`\`\`
|
|
6479
|
+
|
|
6480
|
+
<!-- ::end:framework -->
|
|
6481
|
+
|
|
4979
6482
|
### Advanced i18n with Optional Parameters
|
|
4980
6483
|
|
|
4981
6484
|
Organize i18n routes using optional parameters for flexible locale handling:
|
|
4982
6485
|
|
|
6486
|
+
<!-- ::start:framework -->
|
|
6487
|
+
|
|
6488
|
+
# React
|
|
6489
|
+
|
|
4983
6490
|
\`\`\`tsx
|
|
4984
6491
|
// Route structure:
|
|
4985
6492
|
// routes/
|
|
@@ -5013,11 +6520,52 @@ export const Route = createFileRoute('/{-$locale}/about')({
|
|
|
5013
6520
|
})
|
|
5014
6521
|
\`\`\`
|
|
5015
6522
|
|
|
6523
|
+
# Solid
|
|
6524
|
+
|
|
6525
|
+
\`\`\`tsx
|
|
6526
|
+
// Route structure:
|
|
6527
|
+
// routes/
|
|
6528
|
+
// {-$locale}/
|
|
6529
|
+
// index.tsx // /, /en, /fr
|
|
6530
|
+
// about.tsx // /about, /en/about, /fr/about
|
|
6531
|
+
// blog/
|
|
6532
|
+
// index.tsx // /blog, /en/blog, /fr/blog
|
|
6533
|
+
// $slug.tsx // /blog/post, /en/blog/post, /fr/blog/post
|
|
6534
|
+
|
|
6535
|
+
// routes/{-$locale}/index.tsx
|
|
6536
|
+
export const Route = createFileRoute('/{-$locale}/')({
|
|
6537
|
+
component: HomeComponent,
|
|
6538
|
+
})
|
|
6539
|
+
|
|
6540
|
+
function HomeComponent() {
|
|
6541
|
+
const params = Route.useParams()
|
|
6542
|
+
const isRTL = ['ar', 'he', 'fa'].includes(params().locale || '')
|
|
6543
|
+
|
|
6544
|
+
return (
|
|
6545
|
+
<div dir={isRTL ? 'rtl' : 'ltr'}>
|
|
6546
|
+
<h1>Welcome ({params().locale || 'en'})</h1>
|
|
6547
|
+
{/* Localized content */}
|
|
6548
|
+
</div>
|
|
6549
|
+
)
|
|
6550
|
+
}
|
|
6551
|
+
|
|
6552
|
+
// routes/{-$locale}/about.tsx
|
|
6553
|
+
export const Route = createFileRoute('/{-$locale}/about')({
|
|
6554
|
+
component: AboutComponent,
|
|
6555
|
+
})
|
|
6556
|
+
\`\`\`
|
|
6557
|
+
|
|
6558
|
+
<!-- ::end:framework -->
|
|
6559
|
+
|
|
5016
6560
|
### SEO and Canonical URLs
|
|
5017
6561
|
|
|
5018
6562
|
Handle SEO for i18n routes properly:
|
|
5019
6563
|
|
|
5020
|
-
|
|
6564
|
+
<!-- ::start:framework -->
|
|
6565
|
+
|
|
6566
|
+
# React
|
|
6567
|
+
|
|
6568
|
+
\`\`\`tsx title="src/routes/{-$locale}/products/$id.tsx"
|
|
5021
6569
|
export const Route = createFileRoute('/{-$locale}/products/$id')({
|
|
5022
6570
|
component: ProductComponent,
|
|
5023
6571
|
head: ({ params, loaderData }) => {
|
|
@@ -5064,11 +6612,66 @@ export const Route = createFileRoute('/{-$locale}/products/$id')({
|
|
|
5064
6612
|
})
|
|
5065
6613
|
\`\`\`
|
|
5066
6614
|
|
|
6615
|
+
# Solid
|
|
6616
|
+
|
|
6617
|
+
\`\`\`tsx title="src/routes/{-$locale}/products/$id.tsx"
|
|
6618
|
+
export const Route = createFileRoute('/{-$locale}/products/$id')({
|
|
6619
|
+
component: ProductComponent,
|
|
6620
|
+
head: ({ params, loaderData }) => {
|
|
6621
|
+
const locale = params.locale || 'en'
|
|
6622
|
+
const product = loaderData
|
|
6623
|
+
|
|
6624
|
+
return {
|
|
6625
|
+
title: product.title[locale] || product.title.en,
|
|
6626
|
+
meta: [
|
|
6627
|
+
{
|
|
6628
|
+
name: 'description',
|
|
6629
|
+
content: product.description[locale] || product.description.en,
|
|
6630
|
+
},
|
|
6631
|
+
{
|
|
6632
|
+
property: 'og:locale',
|
|
6633
|
+
content: locale,
|
|
6634
|
+
},
|
|
6635
|
+
],
|
|
6636
|
+
links: [
|
|
6637
|
+
// Canonical URL (always use default locale format)
|
|
6638
|
+
{
|
|
6639
|
+
rel: 'canonical',
|
|
6640
|
+
href: \`https://example.com/products/\${params.id}\`,
|
|
6641
|
+
},
|
|
6642
|
+
// Alternate language versions
|
|
6643
|
+
{
|
|
6644
|
+
rel: 'alternate',
|
|
6645
|
+
hreflang: 'en',
|
|
6646
|
+
href: \`https://example.com/products/\${params.id}\`,
|
|
6647
|
+
},
|
|
6648
|
+
{
|
|
6649
|
+
rel: 'alternate',
|
|
6650
|
+
hreflang: 'fr',
|
|
6651
|
+
href: \`https://example.com/fr/products/\${params.id}\`,
|
|
6652
|
+
},
|
|
6653
|
+
{
|
|
6654
|
+
rel: 'alternate',
|
|
6655
|
+
hreflang: 'es',
|
|
6656
|
+
href: \`https://example.com/es/products/\${params.id}\`,
|
|
6657
|
+
},
|
|
6658
|
+
],
|
|
6659
|
+
}
|
|
6660
|
+
},
|
|
6661
|
+
})
|
|
6662
|
+
\`\`\`
|
|
6663
|
+
|
|
6664
|
+
<!-- ::end:framework -->
|
|
6665
|
+
|
|
5067
6666
|
### Type Safety for i18n
|
|
5068
6667
|
|
|
5069
6668
|
Ensure type safety for your i18n implementations:
|
|
5070
6669
|
|
|
5071
|
-
|
|
6670
|
+
<!-- ::start:framework -->
|
|
6671
|
+
|
|
6672
|
+
# React
|
|
6673
|
+
|
|
6674
|
+
\`\`\`tsx title="src/routes/{-$locale}/shop/{-$category}.tsx"
|
|
5072
6675
|
// Define supported locales
|
|
5073
6676
|
type Locale = 'en' | 'fr' | 'es' | 'de'
|
|
5074
6677
|
|
|
@@ -5118,6 +6721,60 @@ function ShopComponent() {
|
|
|
5118
6721
|
}
|
|
5119
6722
|
\`\`\`
|
|
5120
6723
|
|
|
6724
|
+
# Solid
|
|
6725
|
+
|
|
6726
|
+
\`\`\`tsx title="src/routes/{-$locale}/shop/{-$category}.tsx"
|
|
6727
|
+
// Define supported locales
|
|
6728
|
+
type Locale = 'en' | 'fr' | 'es' | 'de'
|
|
6729
|
+
|
|
6730
|
+
// Type-safe locale validation
|
|
6731
|
+
function validateLocale(locale: string | undefined): locale is Locale {
|
|
6732
|
+
return ['en', 'fr', 'es', 'de'].includes(locale as Locale)
|
|
6733
|
+
}
|
|
6734
|
+
|
|
6735
|
+
export const Route = createFileRoute('/{-$locale}/shop/{-$category}')({
|
|
6736
|
+
beforeLoad: async ({ params }) => {
|
|
6737
|
+
const { locale } = params
|
|
6738
|
+
|
|
6739
|
+
// Type-safe locale validation
|
|
6740
|
+
if (locale && !validateLocale(locale)) {
|
|
6741
|
+
throw redirect({
|
|
6742
|
+
to: '/shop/{-$category}',
|
|
6743
|
+
params: { category: params.category },
|
|
6744
|
+
})
|
|
6745
|
+
}
|
|
6746
|
+
|
|
6747
|
+
return {
|
|
6748
|
+
locale: (locale as Locale) || 'en',
|
|
6749
|
+
isDefaultLocale: !locale || locale === 'en',
|
|
6750
|
+
}
|
|
6751
|
+
},
|
|
6752
|
+
component: ShopComponent,
|
|
6753
|
+
})
|
|
6754
|
+
|
|
6755
|
+
function ShopComponent() {
|
|
6756
|
+
const params = Route.useParams()
|
|
6757
|
+
const routeContext = Route.useRouteContext()
|
|
6758
|
+
|
|
6759
|
+
// TypeScript knows locale is Locale | undefined
|
|
6760
|
+
// and we have validated it in beforeLoad
|
|
6761
|
+
|
|
6762
|
+
return (
|
|
6763
|
+
<div>
|
|
6764
|
+
<h1>Shop {params().category ? \`- \${params().category}\` : ''}</h1>
|
|
6765
|
+
<p>Language: {params().locale || 'en'}</p>
|
|
6766
|
+
{!routeContext().isDefaultLocale && (
|
|
6767
|
+
<Link to="/shop/{-$category}" params={{ category: params().category }}>
|
|
6768
|
+
View in English
|
|
6769
|
+
</Link>
|
|
6770
|
+
)}
|
|
6771
|
+
</div>
|
|
6772
|
+
)
|
|
6773
|
+
}
|
|
6774
|
+
\`\`\`
|
|
6775
|
+
|
|
6776
|
+
<!-- ::end:framework -->
|
|
6777
|
+
|
|
5121
6778
|
Optional path parameters provide a powerful and flexible foundation for implementing internationalization in your TanStack Router applications. Whether you prefer prefix-based or combined approaches, you can create clean, SEO-friendly URLs while maintaining excellent developer experience and type safety.
|
|
5122
6779
|
|
|
5123
6780
|
## Allowed Characters
|
|
@@ -5171,6 +6828,10 @@ If you need more control over preloading, caching and/or garbage collection of p
|
|
|
5171
6828
|
|
|
5172
6829
|
The simplest way to preload routes for your application is to set the \`defaultPreload\` option to \`intent\` for your entire router:
|
|
5173
6830
|
|
|
6831
|
+
<!-- ::start:framework -->
|
|
6832
|
+
|
|
6833
|
+
# React
|
|
6834
|
+
|
|
5174
6835
|
\`\`\`tsx
|
|
5175
6836
|
import { createRouter } from '@tanstack/react-router'
|
|
5176
6837
|
|
|
@@ -5180,12 +6841,29 @@ const router = createRouter({
|
|
|
5180
6841
|
})
|
|
5181
6842
|
\`\`\`
|
|
5182
6843
|
|
|
6844
|
+
# Solid
|
|
6845
|
+
|
|
6846
|
+
\`\`\`tsx
|
|
6847
|
+
import { createRouter } from '@tanstack/solid-router'
|
|
6848
|
+
|
|
6849
|
+
const router = createRouter({
|
|
6850
|
+
// ...
|
|
6851
|
+
defaultPreload: 'intent',
|
|
6852
|
+
})
|
|
6853
|
+
\`\`\`
|
|
6854
|
+
|
|
6855
|
+
<!-- ::end:framework -->
|
|
6856
|
+
|
|
5183
6857
|
This will turn on \`intent\` preloading by default for all \`<Link>\` components in your application. You can also set the \`preload\` prop on individual \`<Link>\` components to override the default behavior.
|
|
5184
6858
|
|
|
5185
6859
|
## Preload Delay
|
|
5186
6860
|
|
|
5187
6861
|
By default, preloading will start after **50ms** of the user hovering or touching a \`<Link>\` component. You can change this delay by setting the \`defaultPreloadDelay\` option on your router:
|
|
5188
6862
|
|
|
6863
|
+
<!-- ::start:framework -->
|
|
6864
|
+
|
|
6865
|
+
# React
|
|
6866
|
+
|
|
5189
6867
|
\`\`\`tsx
|
|
5190
6868
|
import { createRouter } from '@tanstack/react-router'
|
|
5191
6869
|
|
|
@@ -5195,6 +6873,19 @@ const router = createRouter({
|
|
|
5195
6873
|
})
|
|
5196
6874
|
\`\`\`
|
|
5197
6875
|
|
|
6876
|
+
# Solid
|
|
6877
|
+
|
|
6878
|
+
\`\`\`tsx
|
|
6879
|
+
import { createRouter } from '@tanstack/solid-router'
|
|
6880
|
+
|
|
6881
|
+
const router = createRouter({
|
|
6882
|
+
// ...
|
|
6883
|
+
defaultPreloadDelay: 100,
|
|
6884
|
+
})
|
|
6885
|
+
\`\`\`
|
|
6886
|
+
|
|
6887
|
+
<!-- ::end:framework -->
|
|
6888
|
+
|
|
5198
6889
|
You can also set the \`preloadDelay\` prop on individual \`<Link>\` components to override the default behavior on a per-link basis.
|
|
5199
6890
|
|
|
5200
6891
|
## Built-in Preloading & \`preloadStaleTime\`
|
|
@@ -5203,6 +6894,10 @@ If you're using the built-in loaders, you can control how long preloaded data is
|
|
|
5203
6894
|
|
|
5204
6895
|
To change this, you can set the \`defaultPreloadStaleTime\` option on your router:
|
|
5205
6896
|
|
|
6897
|
+
<!-- ::start:framework -->
|
|
6898
|
+
|
|
6899
|
+
# React
|
|
6900
|
+
|
|
5206
6901
|
\`\`\`tsx
|
|
5207
6902
|
import { createRouter } from '@tanstack/react-router'
|
|
5208
6903
|
|
|
@@ -5212,6 +6907,19 @@ const router = createRouter({
|
|
|
5212
6907
|
})
|
|
5213
6908
|
\`\`\`
|
|
5214
6909
|
|
|
6910
|
+
# Solid
|
|
6911
|
+
|
|
6912
|
+
\`\`\`tsx
|
|
6913
|
+
import { createRouter } from '@tanstack/solid-router'
|
|
6914
|
+
|
|
6915
|
+
const router = createRouter({
|
|
6916
|
+
// ...
|
|
6917
|
+
defaultPreloadStaleTime: 10_000,
|
|
6918
|
+
})
|
|
6919
|
+
\`\`\`
|
|
6920
|
+
|
|
6921
|
+
<!-- ::end:framework -->
|
|
6922
|
+
|
|
5215
6923
|
Or, you can use the \`routeOptions.preloadStaleTime\` option on individual routes:
|
|
5216
6924
|
|
|
5217
6925
|
\`\`\`tsx
|
|
@@ -5231,6 +6939,10 @@ To customize the preloading behavior in TanStack Router and fully leverage your
|
|
|
5231
6939
|
|
|
5232
6940
|
For example:
|
|
5233
6941
|
|
|
6942
|
+
<!-- ::start:framework -->
|
|
6943
|
+
|
|
6944
|
+
# React
|
|
6945
|
+
|
|
5234
6946
|
\`\`\`tsx
|
|
5235
6947
|
import { createRouter } from '@tanstack/react-router'
|
|
5236
6948
|
|
|
@@ -5240,12 +6952,29 @@ const router = createRouter({
|
|
|
5240
6952
|
})
|
|
5241
6953
|
\`\`\`
|
|
5242
6954
|
|
|
6955
|
+
# Solid
|
|
6956
|
+
|
|
6957
|
+
\`\`\`tsx
|
|
6958
|
+
import { createRouter } from '@tanstack/solid-router'
|
|
6959
|
+
|
|
6960
|
+
const router = createRouter({
|
|
6961
|
+
// ...
|
|
6962
|
+
defaultPreloadStaleTime: 0,
|
|
6963
|
+
})
|
|
6964
|
+
\`\`\`
|
|
6965
|
+
|
|
6966
|
+
<!-- ::end:framework -->
|
|
6967
|
+
|
|
5243
6968
|
This would then allow you, for instance, to use an option like React Query's \`staleTime\` to control the freshness of your preloads.
|
|
5244
6969
|
|
|
5245
6970
|
## Preloading Manually
|
|
5246
6971
|
|
|
5247
6972
|
If you need to manually preload a route, you can use the router's \`preloadRoute\` method. It accepts a standard TanStack \`NavigateOptions\` object and returns a promise that resolves when the route is preloaded.
|
|
5248
6973
|
|
|
6974
|
+
<!-- ::start:framework -->
|
|
6975
|
+
|
|
6976
|
+
# React
|
|
6977
|
+
|
|
5249
6978
|
\`\`\`tsx
|
|
5250
6979
|
function Component() {
|
|
5251
6980
|
const router = useRouter()
|
|
@@ -5269,8 +6998,39 @@ function Component() {
|
|
|
5269
6998
|
}
|
|
5270
6999
|
\`\`\`
|
|
5271
7000
|
|
|
7001
|
+
# Solid
|
|
7002
|
+
|
|
7003
|
+
\`\`\`tsx
|
|
7004
|
+
function Component() {
|
|
7005
|
+
const router = useRouter()
|
|
7006
|
+
|
|
7007
|
+
createEffect(() => {
|
|
7008
|
+
async function preload() {
|
|
7009
|
+
try {
|
|
7010
|
+
const matches = await router.preloadRoute({
|
|
7011
|
+
to: postRoute,
|
|
7012
|
+
params: { id: 1 },
|
|
7013
|
+
})
|
|
7014
|
+
} catch (err) {
|
|
7015
|
+
// Failed to preload route
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
7018
|
+
|
|
7019
|
+
preload()
|
|
7020
|
+
})
|
|
7021
|
+
|
|
7022
|
+
return <div />
|
|
7023
|
+
}
|
|
7024
|
+
\`\`\`
|
|
7025
|
+
|
|
7026
|
+
<!-- ::end:framework -->
|
|
7027
|
+
|
|
5272
7028
|
If you need to preload only the JS chunk of a route, you can use the router's \`loadRouteChunk\` method. It accepts a route object and returns a promise that resolves when the route chunk is loaded.
|
|
5273
7029
|
|
|
7030
|
+
<!-- ::start:framework -->
|
|
7031
|
+
|
|
7032
|
+
# React
|
|
7033
|
+
|
|
5274
7034
|
\`\`\`tsx
|
|
5275
7035
|
function Component() {
|
|
5276
7036
|
const router = useRouter()
|
|
@@ -5296,6 +7056,35 @@ function Component() {
|
|
|
5296
7056
|
}
|
|
5297
7057
|
\`\`\`
|
|
5298
7058
|
|
|
7059
|
+
# Solid
|
|
7060
|
+
|
|
7061
|
+
\`\`\`tsx
|
|
7062
|
+
function Component() {
|
|
7063
|
+
const router = useRouter()
|
|
7064
|
+
|
|
7065
|
+
createEffect(() => {
|
|
7066
|
+
async function preloadRouteChunks() {
|
|
7067
|
+
try {
|
|
7068
|
+
const postsRoute = router.routesByPath['/posts']
|
|
7069
|
+
await Promise.all([
|
|
7070
|
+
router.loadRouteChunk(router.routesByPath['/']),
|
|
7071
|
+
router.loadRouteChunk(postsRoute),
|
|
7072
|
+
router.loadRouteChunk(postsRoute.parentRoute),
|
|
7073
|
+
])
|
|
7074
|
+
} catch (err) {
|
|
7075
|
+
// Failed to preload route chunk
|
|
7076
|
+
}
|
|
7077
|
+
}
|
|
7078
|
+
|
|
7079
|
+
preloadRouteChunks()
|
|
7080
|
+
})
|
|
7081
|
+
|
|
7082
|
+
return <div />
|
|
7083
|
+
}
|
|
7084
|
+
\`\`\`
|
|
7085
|
+
|
|
7086
|
+
<!-- ::end:framework -->
|
|
7087
|
+
|
|
5299
7088
|
# Render Optimizations
|
|
5300
7089
|
|
|
5301
7090
|
TanStack Router includes several optimizations to ensure your components only re-render when necessary. These optimizations include:
|
|
@@ -5476,7 +7265,9 @@ function onOpenPhoto() {
|
|
|
5476
7265
|
|
|
5477
7266
|
In addition to the imperative API, you can also use the Router's \`routeMasks\` option to declaratively mask routes. Instead of needing to pass the \`mask\` option to every \`<Link>\` or \`navigate()\` call, you can instead create a route mask on the Router to mask routes that match a certain pattern. Here's an example of the same route mask from above, but using the \`routeMasks\` option instead:
|
|
5478
7267
|
|
|
5479
|
-
|
|
7268
|
+
<!-- ::start:framework -->
|
|
7269
|
+
|
|
7270
|
+
# React
|
|
5480
7271
|
|
|
5481
7272
|
\`\`\`tsx
|
|
5482
7273
|
import { createRouteMask } from '@tanstack/react-router'
|
|
@@ -5496,6 +7287,28 @@ const router = createRouter({
|
|
|
5496
7287
|
})
|
|
5497
7288
|
\`\`\`
|
|
5498
7289
|
|
|
7290
|
+
# Solid
|
|
7291
|
+
|
|
7292
|
+
\`\`\`tsx
|
|
7293
|
+
import { createRouteMask } from '@tanstack/solid-router'
|
|
7294
|
+
|
|
7295
|
+
const photoModalToPhotoMask = createRouteMask({
|
|
7296
|
+
routeTree,
|
|
7297
|
+
from: '/photos/$photoId/modal',
|
|
7298
|
+
to: '/photos/$photoId',
|
|
7299
|
+
params: (prev) => ({
|
|
7300
|
+
photoId: prev.photoId,
|
|
7301
|
+
}),
|
|
7302
|
+
})
|
|
7303
|
+
|
|
7304
|
+
const router = createRouter({
|
|
7305
|
+
routeTree,
|
|
7306
|
+
routeMasks: [photoModalToPhotoMask],
|
|
7307
|
+
})
|
|
7308
|
+
\`\`\`
|
|
7309
|
+
|
|
7310
|
+
<!-- ::end:framework -->
|
|
7311
|
+
|
|
5499
7312
|
When creating a route mask, you'll need to pass 1 argument with at least:
|
|
5500
7313
|
|
|
5501
7314
|
- \`routeTree\` - The route tree that the route mask will be applied to
|
|
@@ -5539,6 +7352,10 @@ These are just suggested uses of the router context. You can use it for whatever
|
|
|
5539
7352
|
|
|
5540
7353
|
Like everything else, the root router context is strictly typed. This type can be augmented via any route's \`beforeLoad\` option as it is merged down the route match tree. To constrain the type of the root router context, you must use the \`createRootRouteWithContext<YourContextTypeHere>()(routeOptions)\` function to create a new router context instead of the \`createRootRoute()\` function to create your root route. Here's an example:
|
|
5541
7354
|
|
|
7355
|
+
<!-- ::start:framework -->
|
|
7356
|
+
|
|
7357
|
+
# React
|
|
7358
|
+
|
|
5542
7359
|
\`\`\`tsx
|
|
5543
7360
|
import {
|
|
5544
7361
|
createRootRouteWithContext,
|
|
@@ -5564,6 +7381,35 @@ const router = createRouter({
|
|
|
5564
7381
|
})
|
|
5565
7382
|
\`\`\`
|
|
5566
7383
|
|
|
7384
|
+
# Solid
|
|
7385
|
+
|
|
7386
|
+
\`\`\`tsx
|
|
7387
|
+
import {
|
|
7388
|
+
createRootRouteWithContext,
|
|
7389
|
+
createRouter,
|
|
7390
|
+
} from '@tanstack/solid-router'
|
|
7391
|
+
|
|
7392
|
+
interface MyRouterContext {
|
|
7393
|
+
user: User
|
|
7394
|
+
}
|
|
7395
|
+
|
|
7396
|
+
// Use the routerContext to create your root route
|
|
7397
|
+
const rootRoute = createRootRouteWithContext<MyRouterContext>()({
|
|
7398
|
+
component: App,
|
|
7399
|
+
})
|
|
7400
|
+
|
|
7401
|
+
const routeTree = rootRoute.addChildren([
|
|
7402
|
+
// ...
|
|
7403
|
+
])
|
|
7404
|
+
|
|
7405
|
+
// Use the routerContext to create your router
|
|
7406
|
+
const router = createRouter({
|
|
7407
|
+
routeTree,
|
|
7408
|
+
})
|
|
7409
|
+
\`\`\`
|
|
7410
|
+
|
|
7411
|
+
<!-- ::end:framework -->
|
|
7412
|
+
|
|
5567
7413
|
> [!TIP]
|
|
5568
7414
|
> \`MyRouterContext\` only needs to contain content that will be passed directly to \`createRouter\` below. All other context added in \`beforeLoad\` will be inferred.
|
|
5569
7415
|
|
|
@@ -5574,6 +7420,10 @@ The router context is passed to the router at instantiation time. You can pass t
|
|
|
5574
7420
|
> [!TIP]
|
|
5575
7421
|
> If your context has any required properties, you will see a TypeScript error if you don't pass them in the initial router context. If all of your context properties are optional, you will not see a TypeScript error and passing the context will be optional. If you don't pass a router context, it defaults to \`{}\`.
|
|
5576
7422
|
|
|
7423
|
+
<!-- ::start:framework -->
|
|
7424
|
+
|
|
7425
|
+
# React
|
|
7426
|
+
|
|
5577
7427
|
\`\`\`tsx
|
|
5578
7428
|
import { createRouter } from '@tanstack/react-router'
|
|
5579
7429
|
|
|
@@ -5589,10 +7439,33 @@ const router = createRouter({
|
|
|
5589
7439
|
})
|
|
5590
7440
|
\`\`\`
|
|
5591
7441
|
|
|
7442
|
+
# Solid
|
|
7443
|
+
|
|
7444
|
+
\`\`\`tsx
|
|
7445
|
+
import { createRouter } from '@tanstack/solid-router'
|
|
7446
|
+
|
|
7447
|
+
// Use the routerContext you created to create your router
|
|
7448
|
+
const router = createRouter({
|
|
7449
|
+
routeTree,
|
|
7450
|
+
context: {
|
|
7451
|
+
user: {
|
|
7452
|
+
id: '123',
|
|
7453
|
+
name: 'John Doe',
|
|
7454
|
+
},
|
|
7455
|
+
},
|
|
7456
|
+
})
|
|
7457
|
+
\`\`\`
|
|
7458
|
+
|
|
7459
|
+
<!-- ::end:framework -->
|
|
7460
|
+
|
|
5592
7461
|
### Invalidating the Router Context
|
|
5593
7462
|
|
|
5594
7463
|
If you need to invalidate the context state you are passing into the router, you can call the \`invalidate\` method to tell the router to recompute the context. This is useful when you need to update the context state and have the router recompute the context for all routes.
|
|
5595
7464
|
|
|
7465
|
+
<!-- ::start:framework -->
|
|
7466
|
+
|
|
7467
|
+
# React
|
|
7468
|
+
|
|
5596
7469
|
\`\`\`tsx
|
|
5597
7470
|
function useAuth() {
|
|
5598
7471
|
const router = useRouter()
|
|
@@ -5611,6 +7484,28 @@ function useAuth() {
|
|
|
5611
7484
|
}
|
|
5612
7485
|
\`\`\`
|
|
5613
7486
|
|
|
7487
|
+
# Solid
|
|
7488
|
+
|
|
7489
|
+
\`\`\`tsx
|
|
7490
|
+
function useAuth() {
|
|
7491
|
+
const router = useRouter()
|
|
7492
|
+
const [user, setUser] = createSignal<User | null>(null)
|
|
7493
|
+
|
|
7494
|
+
createEffect(() => {
|
|
7495
|
+
const unsubscribe = auth.onAuthStateChanged((user) => {
|
|
7496
|
+
setUser(user)
|
|
7497
|
+
router.invalidate()
|
|
7498
|
+
})
|
|
7499
|
+
|
|
7500
|
+
return unsubscribe
|
|
7501
|
+
}, [])
|
|
7502
|
+
|
|
7503
|
+
return user()
|
|
7504
|
+
}
|
|
7505
|
+
\`\`\`
|
|
7506
|
+
|
|
7507
|
+
<!-- ::end:framework -->
|
|
7508
|
+
|
|
5614
7509
|
## Using the Router Context
|
|
5615
7510
|
|
|
5616
7511
|
Once you have defined the router context type, you can use it in your route definitions:
|
|
@@ -5655,6 +7550,10 @@ export const Route = createFileRoute('/todos')({
|
|
|
5655
7550
|
|
|
5656
7551
|
### How about an external data fetching library?
|
|
5657
7552
|
|
|
7553
|
+
<!-- ::start:framework -->
|
|
7554
|
+
|
|
7555
|
+
# React
|
|
7556
|
+
|
|
5658
7557
|
\`\`\`tsx
|
|
5659
7558
|
import {
|
|
5660
7559
|
createRootRouteWithContext,
|
|
@@ -5679,6 +7578,34 @@ const router = createRouter({
|
|
|
5679
7578
|
})
|
|
5680
7579
|
\`\`\`
|
|
5681
7580
|
|
|
7581
|
+
# Solid
|
|
7582
|
+
|
|
7583
|
+
\`\`\`tsx
|
|
7584
|
+
import {
|
|
7585
|
+
createRootRouteWithContext,
|
|
7586
|
+
createRouter,
|
|
7587
|
+
} from '@tanstack/solid-router'
|
|
7588
|
+
|
|
7589
|
+
interface MyRouterContext {
|
|
7590
|
+
queryClient: QueryClient
|
|
7591
|
+
}
|
|
7592
|
+
|
|
7593
|
+
const rootRoute = createRootRouteWithContext<MyRouterContext>()({
|
|
7594
|
+
component: App,
|
|
7595
|
+
})
|
|
7596
|
+
|
|
7597
|
+
const queryClient = new QueryClient()
|
|
7598
|
+
|
|
7599
|
+
const router = createRouter({
|
|
7600
|
+
routeTree: rootRoute,
|
|
7601
|
+
context: {
|
|
7602
|
+
queryClient,
|
|
7603
|
+
},
|
|
7604
|
+
})
|
|
7605
|
+
\`\`\`
|
|
7606
|
+
|
|
7607
|
+
<!-- ::end:framework -->
|
|
7608
|
+
|
|
5682
7609
|
Then, in your route:
|
|
5683
7610
|
|
|
5684
7611
|
\`\`\`tsx
|
|
@@ -5694,6 +7621,10 @@ export const Route = createFileRoute('/todos')({
|
|
|
5694
7621
|
})
|
|
5695
7622
|
\`\`\`
|
|
5696
7623
|
|
|
7624
|
+
<!-- ::start:framework -->
|
|
7625
|
+
|
|
7626
|
+
# React
|
|
7627
|
+
|
|
5697
7628
|
## How about using React Context/Hooks?
|
|
5698
7629
|
|
|
5699
7630
|
When trying to use React Context or Hooks in your route's \`beforeLoad\` or \`loader\` functions, it's important to remember React's [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks). You can't use hooks in a non-React function, so you can't use hooks in your \`beforeLoad\` or \`loader\` functions.
|
|
@@ -5702,9 +7633,9 @@ So, how do we use React Context or Hooks in our route's \`beforeLoad\` or \`load
|
|
|
5702
7633
|
|
|
5703
7634
|
Let's look at the setup for an example, where we pass down a \`useNetworkStrength\` hook to our route's \`loader\` function:
|
|
5704
7635
|
|
|
5705
|
-
|
|
7636
|
+
<!-- ::start:tabs variant="files" -->
|
|
5706
7637
|
|
|
5707
|
-
\`\`\`tsx
|
|
7638
|
+
\`\`\`tsx title="src/routes/__root.tsx"
|
|
5708
7639
|
// First, make sure the context for the root route is typed
|
|
5709
7640
|
import { createRootRouteWithContext } from '@tanstack/react-router'
|
|
5710
7641
|
import { useNetworkStrength } from '@/hooks/useNetworkStrength'
|
|
@@ -5718,11 +7649,13 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
|
|
|
5718
7649
|
})
|
|
5719
7650
|
\`\`\`
|
|
5720
7651
|
|
|
7652
|
+
<!-- ::end:tabs -->
|
|
7653
|
+
|
|
5721
7654
|
In this example, we'd instantiate the hook before rendering the router using the \`<RouterProvider />\`. This way, the hook would be called in React-land, therefore adhering to the Rules of Hooks.
|
|
5722
7655
|
|
|
5723
|
-
|
|
7656
|
+
<!-- ::start:tabs variant="files" -->
|
|
5724
7657
|
|
|
5725
|
-
\`\`\`tsx
|
|
7658
|
+
\`\`\`tsx title="src/router.tsx"
|
|
5726
7659
|
import { createRouter } from '@tanstack/react-router'
|
|
5727
7660
|
|
|
5728
7661
|
import { routeTree } from './routeTree.gen'
|
|
@@ -5735,9 +7668,13 @@ export const router = createRouter({
|
|
|
5735
7668
|
})
|
|
5736
7669
|
\`\`\`
|
|
5737
7670
|
|
|
5738
|
-
|
|
7671
|
+
<!-- ::end:tabs -->
|
|
5739
7672
|
|
|
5740
|
-
|
|
7673
|
+
Then, we can call the \`useNetworkStrength\` hook in our \`App\` component and pass the returned value into the router context via the \`<RouterProvider />\`:
|
|
7674
|
+
|
|
7675
|
+
<!-- ::start:tabs variant="files" -->
|
|
7676
|
+
|
|
7677
|
+
\`\`\`tsx title="src/main.tsx"
|
|
5741
7678
|
import { RouterProvider } from '@tanstack/react-router'
|
|
5742
7679
|
import { router } from './router'
|
|
5743
7680
|
|
|
@@ -5752,11 +7689,13 @@ function App() {
|
|
|
5752
7689
|
// ...
|
|
5753
7690
|
\`\`\`
|
|
5754
7691
|
|
|
7692
|
+
<!-- ::end:tabs -->
|
|
7693
|
+
|
|
5755
7694
|
So, now in our route's \`loader\` function, we can access the \`networkStrength\` hook from the router context:
|
|
5756
7695
|
|
|
5757
|
-
|
|
7696
|
+
<!-- ::start:tabs variant="files" -->
|
|
5758
7697
|
|
|
5759
|
-
\`\`\`tsx
|
|
7698
|
+
\`\`\`tsx title="src/routes/posts.tsx"
|
|
5760
7699
|
import { createFileRoute } from '@tanstack/react-router'
|
|
5761
7700
|
|
|
5762
7701
|
export const Route = createFileRoute('/posts')({
|
|
@@ -5769,13 +7708,21 @@ export const Route = createFileRoute('/posts')({
|
|
|
5769
7708
|
})
|
|
5770
7709
|
\`\`\`
|
|
5771
7710
|
|
|
7711
|
+
<!-- ::end:tabs -->
|
|
7712
|
+
|
|
7713
|
+
<!-- ::end:framework -->
|
|
7714
|
+
|
|
5772
7715
|
## Modifying the Router Context
|
|
5773
7716
|
|
|
5774
7717
|
The router context is passed down the route tree and is merged at each route. This means that you can modify the context at each route and the modifications will be available to all child routes. Here's an example:
|
|
5775
7718
|
|
|
5776
|
-
|
|
7719
|
+
<!-- ::start:framework -->
|
|
5777
7720
|
|
|
5778
|
-
|
|
7721
|
+
# React
|
|
7722
|
+
|
|
7723
|
+
<!-- ::start:tabs variant="files" -->
|
|
7724
|
+
|
|
7725
|
+
\`\`\`tsx title="src/routes/__root.tsx"
|
|
5779
7726
|
import { createRootRouteWithContext } from '@tanstack/react-router'
|
|
5780
7727
|
|
|
5781
7728
|
interface MyRouterContext {
|
|
@@ -5787,9 +7734,11 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
|
|
|
5787
7734
|
})
|
|
5788
7735
|
\`\`\`
|
|
5789
7736
|
|
|
5790
|
-
|
|
7737
|
+
<!-- ::end:tabs -->
|
|
5791
7738
|
|
|
5792
|
-
|
|
7739
|
+
<!-- ::start:tabs variant="files" -->
|
|
7740
|
+
|
|
7741
|
+
\`\`\`tsx title="src/router.tsx"
|
|
5793
7742
|
import { createRouter } from '@tanstack/react-router'
|
|
5794
7743
|
|
|
5795
7744
|
import { routeTree } from './routeTree.gen'
|
|
@@ -5802,9 +7751,11 @@ const router = createRouter({
|
|
|
5802
7751
|
})
|
|
5803
7752
|
\`\`\`
|
|
5804
7753
|
|
|
5805
|
-
|
|
7754
|
+
<!-- ::end:tabs -->
|
|
5806
7755
|
|
|
5807
|
-
|
|
7756
|
+
<!-- ::start:tabs variant="files" -->
|
|
7757
|
+
|
|
7758
|
+
\`\`\`tsx title="src/routes/todos.tsx"
|
|
5808
7759
|
import { createFileRoute } from '@tanstack/react-router'
|
|
5809
7760
|
|
|
5810
7761
|
export const Route = createFileRoute('/todos')({
|
|
@@ -5821,12 +7772,71 @@ export const Route = createFileRoute('/todos')({
|
|
|
5821
7772
|
})
|
|
5822
7773
|
\`\`\`
|
|
5823
7774
|
|
|
7775
|
+
<!-- ::end:tabs -->
|
|
7776
|
+
|
|
7777
|
+
# Solid
|
|
7778
|
+
|
|
7779
|
+
<!-- ::start:tabs variant="files" -->
|
|
7780
|
+
|
|
7781
|
+
\`\`\`tsx title="src/routes/__root.tsx"
|
|
7782
|
+
import { createRootRouteWithContext } from '@tanstack/solid-router'
|
|
7783
|
+
|
|
7784
|
+
interface MyRouterContext {
|
|
7785
|
+
foo: boolean
|
|
7786
|
+
}
|
|
7787
|
+
|
|
7788
|
+
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
|
7789
|
+
component: App,
|
|
7790
|
+
})
|
|
7791
|
+
\`\`\`
|
|
7792
|
+
|
|
7793
|
+
<!-- ::end:tabs -->
|
|
7794
|
+
|
|
7795
|
+
<!-- ::start:tabs variant="files" -->
|
|
7796
|
+
|
|
7797
|
+
\`\`\`tsx title="src/router.tsx"
|
|
7798
|
+
import { createRouter } from '@tanstack/solid-router'
|
|
7799
|
+
|
|
7800
|
+
import { routeTree } from './routeTree.gen'
|
|
7801
|
+
|
|
7802
|
+
const router = createRouter({
|
|
7803
|
+
routeTree,
|
|
7804
|
+
context: {
|
|
7805
|
+
foo: true,
|
|
7806
|
+
},
|
|
7807
|
+
})
|
|
7808
|
+
\`\`\`
|
|
7809
|
+
|
|
7810
|
+
<!-- ::end:tabs -->
|
|
7811
|
+
|
|
7812
|
+
<!-- ::start:tabs variant="files" -->
|
|
7813
|
+
|
|
7814
|
+
\`\`\`tsx title="src/routes/todos.tsx"
|
|
7815
|
+
import { createFileRoute } from '@tanstack/solid-router'
|
|
7816
|
+
|
|
7817
|
+
export const Route = createFileRoute('/todos')({
|
|
7818
|
+
component: Todos,
|
|
7819
|
+
beforeLoad: () => {
|
|
7820
|
+
return {
|
|
7821
|
+
bar: true,
|
|
7822
|
+
}
|
|
7823
|
+
},
|
|
7824
|
+
loader: ({ context }) => {
|
|
7825
|
+
context.foo // true
|
|
7826
|
+
context.bar // true
|
|
7827
|
+
},
|
|
7828
|
+
})
|
|
7829
|
+
\`\`\`
|
|
7830
|
+
|
|
7831
|
+
<!-- ::end:tabs -->
|
|
7832
|
+
|
|
7833
|
+
<!-- ::end:framework -->
|
|
7834
|
+
|
|
5824
7835
|
## Processing Accumulated Route Context
|
|
5825
7836
|
|
|
5826
7837
|
Context, especially the isolated route \`context\` objects, make it trivial to accumulate and process the route context objects for all matched routes. Here's an example where we use all of the matched route contexts to generate a breadcrumb trail:
|
|
5827
7838
|
|
|
5828
|
-
\`\`\`tsx
|
|
5829
|
-
// src/routes/__root.tsx
|
|
7839
|
+
\`\`\`tsx title="src/routes/__root.tsx"
|
|
5830
7840
|
export const Route = createRootRoute({
|
|
5831
7841
|
component: () => {
|
|
5832
7842
|
const matches = useRouterState({ select: (s) => s.matches })
|
|
@@ -5847,8 +7857,7 @@ export const Route = createRootRoute({
|
|
|
5847
7857
|
|
|
5848
7858
|
Using that same route context, we could also generate a title tag for our page's \`<head>\`:
|
|
5849
7859
|
|
|
5850
|
-
\`\`\`tsx
|
|
5851
|
-
// src/routes/__root.tsx
|
|
7860
|
+
\`\`\`tsx title="src/routes/__root.tsx"
|
|
5852
7861
|
export const Route = createRootRoute({
|
|
5853
7862
|
component: () => {
|
|
5854
7863
|
const matches = useRouterState({ select: (s) => s.matches })
|
|
@@ -5925,8 +7934,6 @@ It does this by:
|
|
|
5925
7934
|
That may sound like a lot, but for you, it's as simple as this:
|
|
5926
7935
|
|
|
5927
7936
|
\`\`\`tsx
|
|
5928
|
-
import { createRouter } from '@tanstack/react-router'
|
|
5929
|
-
|
|
5930
7937
|
const router = createRouter({
|
|
5931
7938
|
scrollRestoration: true,
|
|
5932
7939
|
})
|
|
@@ -5950,8 +7957,6 @@ The default \`getKey\` is \`(location) => location.state.__TSR_key!\`, where \`_
|
|
|
5950
7957
|
You could sync scrolling to the pathname:
|
|
5951
7958
|
|
|
5952
7959
|
\`\`\`tsx
|
|
5953
|
-
import { createRouter } from '@tanstack/react-router'
|
|
5954
|
-
|
|
5955
7960
|
const router = createRouter({
|
|
5956
7961
|
getScrollRestorationKey: (location) => location.pathname,
|
|
5957
7962
|
})
|
|
@@ -5960,8 +7965,6 @@ const router = createRouter({
|
|
|
5960
7965
|
You can conditionally sync only some paths, then use the key for the rest:
|
|
5961
7966
|
|
|
5962
7967
|
\`\`\`tsx
|
|
5963
|
-
import { createRouter } from '@tanstack/react-router'
|
|
5964
|
-
|
|
5965
7968
|
const router = createRouter({
|
|
5966
7969
|
getScrollRestorationKey: (location) => {
|
|
5967
7970
|
const paths = ['/', '/chat']
|
|
@@ -5988,8 +7991,6 @@ Most of the time, you won't need to do anything special to get scroll restoratio
|
|
|
5988
7991
|
|
|
5989
7992
|
To manually control scroll restoration for virtualized lists within the whole browser window:
|
|
5990
7993
|
|
|
5991
|
-
[//]: # 'VirtualizedWindowScrollRestorationExample'
|
|
5992
|
-
|
|
5993
7994
|
\`\`\`tsx
|
|
5994
7995
|
function Component() {
|
|
5995
7996
|
const scrollEntry = useElementScrollRestoration({
|
|
@@ -6015,11 +8016,11 @@ function Component() {
|
|
|
6015
8016
|
}
|
|
6016
8017
|
\`\`\`
|
|
6017
8018
|
|
|
6018
|
-
[//]: # 'VirtualizedWindowScrollRestorationExample'
|
|
6019
|
-
|
|
6020
8019
|
To manually control scroll restoration for a specific element, you can use the \`useElementScrollRestoration\` hook and the \`data-scroll-restoration-id\` DOM attribute:
|
|
6021
8020
|
|
|
6022
|
-
|
|
8021
|
+
<!-- ::start:framework -->
|
|
8022
|
+
|
|
8023
|
+
# React
|
|
6023
8024
|
|
|
6024
8025
|
\`\`\`tsx
|
|
6025
8026
|
function Component() {
|
|
@@ -6058,15 +8059,52 @@ function Component() {
|
|
|
6058
8059
|
}
|
|
6059
8060
|
\`\`\`
|
|
6060
8061
|
|
|
6061
|
-
|
|
8062
|
+
# Solid
|
|
8063
|
+
|
|
8064
|
+
\`\`\`tsx
|
|
8065
|
+
function Component() {
|
|
8066
|
+
// We need a unique ID for manual scroll restoration on a specific element
|
|
8067
|
+
// It should be as unique as possible for this element across your app
|
|
8068
|
+
const scrollRestorationId = 'myVirtualizedContent'
|
|
8069
|
+
|
|
8070
|
+
// We use that ID to get the scroll entry for this element
|
|
8071
|
+
const scrollEntry = useElementScrollRestoration({
|
|
8072
|
+
id: scrollRestorationId,
|
|
8073
|
+
})
|
|
8074
|
+
|
|
8075
|
+
// Let's use TanStack Virtual to virtualize some content!
|
|
8076
|
+
let virtualizerParentRef: any
|
|
8077
|
+
const virtualizer = createVirtualizer({
|
|
8078
|
+
count: 10000,
|
|
8079
|
+
getScrollElement: () => virtualizerParentRef,
|
|
8080
|
+
estimateSize: () => 100,
|
|
8081
|
+
// We pass the scrollY from the scroll restoration entry to the virtualizer
|
|
8082
|
+
// as the initial offset
|
|
8083
|
+
initialOffset: scrollEntry?.scrollY,
|
|
8084
|
+
})
|
|
8085
|
+
|
|
8086
|
+
return (
|
|
8087
|
+
<div
|
|
8088
|
+
ref={virtualizerParentRef}
|
|
8089
|
+
// We pass the scroll restoration ID to the element
|
|
8090
|
+
// as a custom attribute that will get picked up by the
|
|
8091
|
+
// scroll restoration watcher
|
|
8092
|
+
data-scroll-restoration-id={scrollRestorationId}
|
|
8093
|
+
class="flex-1 border rounded-lg overflow-auto relative"
|
|
8094
|
+
>
|
|
8095
|
+
...
|
|
8096
|
+
</div>
|
|
8097
|
+
)
|
|
8098
|
+
}
|
|
8099
|
+
\`\`\`
|
|
8100
|
+
|
|
8101
|
+
<!-- ::end:framework -->
|
|
6062
8102
|
|
|
6063
8103
|
## Scroll Behavior
|
|
6064
8104
|
|
|
6065
8105
|
To control the scroll behavior when navigating between pages, you can use the \`scrollRestorationBehavior\` option. This allows you to make the transition between pages instant instead of a smooth scroll. The global configuration of scroll restoration behavior has the same options as those supported by the browser, which are \`smooth\`, \`instant\`, and \`auto\` (see [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView#behavior) for more information).
|
|
6066
8106
|
|
|
6067
8107
|
\`\`\`tsx
|
|
6068
|
-
import { createRouter } from '@tanstack/react-router'
|
|
6069
|
-
|
|
6070
8108
|
const router = createRouter({
|
|
6071
8109
|
scrollRestorationBehavior: 'instant',
|
|
6072
8110
|
})
|
|
@@ -6162,9 +8200,7 @@ Despite TanStack Router being able to parse search params into reliable JSON, th
|
|
|
6162
8200
|
|
|
6163
8201
|
TanStack Router provides convenient APIs for validating and typing search params. This all starts with the \`Route\`'s \`validateSearch\` option:
|
|
6164
8202
|
|
|
6165
|
-
\`\`\`tsx
|
|
6166
|
-
// /routes/shop.products.tsx
|
|
6167
|
-
|
|
8203
|
+
\`\`\`tsx title="src/routes/shop/products.tsx"
|
|
6168
8204
|
type ProductSearchSortOptions = 'newest' | 'oldest' | 'price'
|
|
6169
8205
|
|
|
6170
8206
|
type ProductSearch = {
|
|
@@ -6193,9 +8229,7 @@ The \`validateSearch\` option is a function that is provided the JSON parsed (bu
|
|
|
6193
8229
|
|
|
6194
8230
|
Here's an example:
|
|
6195
8231
|
|
|
6196
|
-
\`\`\`tsx
|
|
6197
|
-
// /routes/shop.products.tsx
|
|
6198
|
-
|
|
8232
|
+
\`\`\`tsx title="src/routes/shop/products.tsx"
|
|
6199
8233
|
type ProductSearchSortOptions = 'newest' | 'oldest' | 'price'
|
|
6200
8234
|
|
|
6201
8235
|
type ProductSearch = {
|
|
@@ -6218,9 +8252,7 @@ export const Route = createFileRoute('/shop/products')({
|
|
|
6218
8252
|
|
|
6219
8253
|
Here's an example using the [Zod](https://zod.dev/) library (but feel free to use any validation library you want) to both validate and type the search params in a single step:
|
|
6220
8254
|
|
|
6221
|
-
\`\`\`tsx
|
|
6222
|
-
// /routes/shop.products.tsx
|
|
6223
|
-
|
|
8255
|
+
\`\`\`tsx title="src/routes/shop/products.tsx"
|
|
6224
8256
|
import { z } from 'zod'
|
|
6225
8257
|
|
|
6226
8258
|
const productSearchSchema = z.object({
|
|
@@ -6251,7 +8283,6 @@ The underlying mechanics why this works relies on the \`validateSearch\` functio
|
|
|
6251
8283
|
When using a library like [Zod](https://zod.dev/) to validate search params you might want to \`transform\` search params before committing the search params to the URL. A common \`zod\` \`transform\` is \`default\` for example.
|
|
6252
8284
|
|
|
6253
8285
|
\`\`\`tsx
|
|
6254
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
6255
8286
|
import { z } from 'zod'
|
|
6256
8287
|
|
|
6257
8288
|
const productSearchSchema = z.object({
|
|
@@ -6278,7 +8309,6 @@ For validation libraries we recommend using adapters which infer the correct \`i
|
|
|
6278
8309
|
An adapter is provided for [Zod](https://zod.dev/) which will pipe through the correct \`input\` type and \`output\` type
|
|
6279
8310
|
|
|
6280
8311
|
\`\`\`tsx
|
|
6281
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
6282
8312
|
import { zodValidator } from '@tanstack/zod-adapter'
|
|
6283
8313
|
import { z } from 'zod'
|
|
6284
8314
|
|
|
@@ -6302,7 +8332,6 @@ The important part here is the following use of \`Link\` no longer requires \`se
|
|
|
6302
8332
|
However the use of \`catch\` here overrides the types and makes \`page\`, \`filter\` and \`sort\` \`unknown\` causing type loss. We have handled this case by providing a \`fallback\` generic function which retains the types but provides a \`fallback\` value when validation fails
|
|
6303
8333
|
|
|
6304
8334
|
\`\`\`tsx
|
|
6305
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
6306
8335
|
import { fallback, zodValidator } from '@tanstack/zod-adapter'
|
|
6307
8336
|
import { z } from 'zod'
|
|
6308
8337
|
|
|
@@ -6351,7 +8380,6 @@ This provides flexibility in which type you want to infer for navigation and whi
|
|
|
6351
8380
|
When using [Valibot](https://valibot.dev/) an adapter is not needed to ensure the correct \`input\` and \`output\` types are used for navigation and reading search params. This is because \`valibot\` implements [Standard Schema](https://github.com/standard-schema/standard-schema)
|
|
6352
8381
|
|
|
6353
8382
|
\`\`\`tsx
|
|
6354
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
6355
8383
|
import * as v from 'valibot'
|
|
6356
8384
|
|
|
6357
8385
|
const productSearchSchema = v.object({
|
|
@@ -6376,7 +8404,6 @@ export const Route = createFileRoute('/shop/products/')({
|
|
|
6376
8404
|
When using [ArkType](https://arktype.io/) an adapter is not needed to ensure the correct \`input\` and \`output\` types are used for navigation and reading search params. This is because [ArkType](https://arktype.io/) implements [Standard Schema](https://github.com/standard-schema/standard-schema)
|
|
6377
8405
|
|
|
6378
8406
|
\`\`\`tsx
|
|
6379
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
6380
8407
|
import { type } from 'arktype'
|
|
6381
8408
|
|
|
6382
8409
|
const productSearchSchema = type({
|
|
@@ -6395,7 +8422,6 @@ export const Route = createFileRoute('/shop/products/')({
|
|
|
6395
8422
|
When using [Effect/Schema](https://effect.website/docs/schema/introduction/) an adapter is not needed to ensure the correct \`input\` and \`output\` types are used for navigation and reading search params. This is because [Effect/Schema](https://effect.website/docs/schema/standard-schema/) implements [Standard Schema](https://github.com/standard-schema/standard-schema)
|
|
6396
8423
|
|
|
6397
8424
|
\`\`\`tsx
|
|
6398
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
6399
8425
|
import { Schema as S } from 'effect'
|
|
6400
8426
|
|
|
6401
8427
|
const productSearchSchema = S.standardSchemaV1(
|
|
@@ -6441,9 +8467,9 @@ Please read the [Search Params in Loaders](./data-loading.md#using-loaderdeps-to
|
|
|
6441
8467
|
|
|
6442
8468
|
The search parameters and types of parents are merged as you go down the route tree, so child routes also have access to their parent's search params:
|
|
6443
8469
|
|
|
6444
|
-
|
|
8470
|
+
<!-- ::start:tabs variant="files" -->
|
|
6445
8471
|
|
|
6446
|
-
\`\`\`tsx
|
|
8472
|
+
\`\`\`tsx title="src/routes/shop/products.tsx"
|
|
6447
8473
|
const productSearchSchema = z.object({
|
|
6448
8474
|
page: z.number().catch(1),
|
|
6449
8475
|
filter: z.string().catch(''),
|
|
@@ -6457,9 +8483,7 @@ export const Route = createFileRoute('/shop/products')({
|
|
|
6457
8483
|
})
|
|
6458
8484
|
\`\`\`
|
|
6459
8485
|
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
\`\`\`tsx
|
|
8486
|
+
\`\`\`tsx title="src/routes/shop/products/$productId.tsx"
|
|
6463
8487
|
export const Route = createFileRoute('/shop/products/$productId')({
|
|
6464
8488
|
beforeLoad: ({ search }) => {
|
|
6465
8489
|
search
|
|
@@ -6468,13 +8492,13 @@ export const Route = createFileRoute('/shop/products/$productId')({
|
|
|
6468
8492
|
})
|
|
6469
8493
|
\`\`\`
|
|
6470
8494
|
|
|
8495
|
+
<!-- ::end:tabs -->
|
|
8496
|
+
|
|
6471
8497
|
### Search Params in Components
|
|
6472
8498
|
|
|
6473
8499
|
You can access your route's validated search params in your route's \`component\` via the \`useSearch\` hook.
|
|
6474
8500
|
|
|
6475
|
-
\`\`\`tsx
|
|
6476
|
-
// /routes/shop.products.tsx
|
|
6477
|
-
|
|
8501
|
+
\`\`\`tsx title="src/routes/shop/products.tsx"
|
|
6478
8502
|
export const Route = createFileRoute('/shop/products')({
|
|
6479
8503
|
validateSearch: productSearchSchema,
|
|
6480
8504
|
})
|
|
@@ -6494,7 +8518,7 @@ const ProductList = () => {
|
|
|
6494
8518
|
You can access your route's validated search params anywhere in your app using the \`useSearch\` hook. By passing the \`from\` id/path of your origin route, you'll get even better type safety:
|
|
6495
8519
|
|
|
6496
8520
|
\`\`\`tsx
|
|
6497
|
-
// /routes/shop.products.tsx
|
|
8521
|
+
// src/routes/shop.products.tsx
|
|
6498
8522
|
export const Route = createFileRoute('/shop/products')({
|
|
6499
8523
|
validateSearch: productSearchSchema,
|
|
6500
8524
|
// ...
|
|
@@ -6502,7 +8526,7 @@ export const Route = createFileRoute('/shop/products')({
|
|
|
6502
8526
|
|
|
6503
8527
|
// Somewhere else...
|
|
6504
8528
|
|
|
6505
|
-
// /components/product-list-sidebar.tsx
|
|
8529
|
+
// src/components/product-list-sidebar.tsx
|
|
6506
8530
|
const routeApi = getRouteApi('/shop/products')
|
|
6507
8531
|
|
|
6508
8532
|
const ProductList = () => {
|
|
@@ -6546,8 +8570,7 @@ The best way to update search params is to use the \`search\` prop on the \`<Lin
|
|
|
6546
8570
|
If the search for the current page shall be updated and the \`from\` prop is specified, the \`to\` prop can be omitted.
|
|
6547
8571
|
Here's an example:
|
|
6548
8572
|
|
|
6549
|
-
\`\`\`tsx
|
|
6550
|
-
// /routes/shop.products.tsx
|
|
8573
|
+
\`\`\`tsx title="src/routes/shop/products.tsx"
|
|
6551
8574
|
export const Route = createFileRoute('/shop/products')({
|
|
6552
8575
|
validateSearch: productSearchSchema,
|
|
6553
8576
|
})
|
|
@@ -6603,8 +8626,7 @@ const PageSelector = () => {
|
|
|
6603
8626
|
|
|
6604
8627
|
The \`navigate\` function also accepts a \`search\` option that works the same way as the \`search\` prop on \`<Link />\`:
|
|
6605
8628
|
|
|
6606
|
-
\`\`\`tsx
|
|
6607
|
-
// /routes/shop.products.tsx
|
|
8629
|
+
\`\`\`tsx title="src/routes/shop/products.tsx"
|
|
6608
8630
|
export const Route = createFileRoute('/shop/products/$productId')({
|
|
6609
8631
|
validateSearch: productSearchSchema,
|
|
6610
8632
|
})
|
|
@@ -6648,7 +8670,6 @@ The following example shows how to make sure that for **every** link that is bei
|
|
|
6648
8670
|
|
|
6649
8671
|
\`\`\`tsx
|
|
6650
8672
|
import { z } from 'zod'
|
|
6651
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
6652
8673
|
import { zodValidator } from '@tanstack/zod-adapter'
|
|
6653
8674
|
|
|
6654
8675
|
const searchSchema = z.object({
|
|
@@ -6673,6 +8694,10 @@ export const Route = createRootRoute({
|
|
|
6673
8694
|
|
|
6674
8695
|
Since this specific use case is quite common, TanStack Router provides a generic implementation to retain search params via \`retainSearchParams\`:
|
|
6675
8696
|
|
|
8697
|
+
<!-- ::start:framework -->
|
|
8698
|
+
|
|
8699
|
+
# React
|
|
8700
|
+
|
|
6676
8701
|
\`\`\`tsx
|
|
6677
8702
|
import { z } from 'zod'
|
|
6678
8703
|
import { createFileRoute, retainSearchParams } from '@tanstack/react-router'
|
|
@@ -6690,8 +8715,33 @@ export const Route = createRootRoute({
|
|
|
6690
8715
|
})
|
|
6691
8716
|
\`\`\`
|
|
6692
8717
|
|
|
8718
|
+
# Solid
|
|
8719
|
+
|
|
8720
|
+
\`\`\`tsx
|
|
8721
|
+
import { z } from 'zod'
|
|
8722
|
+
import { createFileRoute, retainSearchParams } from '@tanstack/solid-router'
|
|
8723
|
+
import { zodValidator } from '@tanstack/zod-adapter'
|
|
8724
|
+
|
|
8725
|
+
const searchSchema = z.object({
|
|
8726
|
+
rootValue: z.string().optional(),
|
|
8727
|
+
})
|
|
8728
|
+
|
|
8729
|
+
export const Route = createRootRoute({
|
|
8730
|
+
validateSearch: zodValidator(searchSchema),
|
|
8731
|
+
search: {
|
|
8732
|
+
middlewares: [retainSearchParams(['rootValue'])],
|
|
8733
|
+
},
|
|
8734
|
+
})
|
|
8735
|
+
\`\`\`
|
|
8736
|
+
|
|
8737
|
+
<!-- ::end:framework -->
|
|
8738
|
+
|
|
6693
8739
|
Another common use case is to strip out search params from links if their default value is set. TanStack Router provides a generic implementation for this use case via \`stripSearchParams\`:
|
|
6694
8740
|
|
|
8741
|
+
<!-- ::start:framework -->
|
|
8742
|
+
|
|
8743
|
+
# React
|
|
8744
|
+
|
|
6695
8745
|
\`\`\`tsx
|
|
6696
8746
|
import { z } from 'zod'
|
|
6697
8747
|
import { createFileRoute, stripSearchParams } from '@tanstack/react-router'
|
|
@@ -6716,8 +8766,40 @@ export const Route = createFileRoute('/hello')({
|
|
|
6716
8766
|
})
|
|
6717
8767
|
\`\`\`
|
|
6718
8768
|
|
|
8769
|
+
# Solid
|
|
8770
|
+
|
|
8771
|
+
\`\`\`tsx
|
|
8772
|
+
import { z } from 'zod'
|
|
8773
|
+
import { createFileRoute, stripSearchParams } from '@tanstack/solid-router'
|
|
8774
|
+
import { zodValidator } from '@tanstack/zod-adapter'
|
|
8775
|
+
|
|
8776
|
+
const defaultValues = {
|
|
8777
|
+
one: 'abc',
|
|
8778
|
+
two: 'xyz',
|
|
8779
|
+
}
|
|
8780
|
+
|
|
8781
|
+
const searchSchema = z.object({
|
|
8782
|
+
one: z.string().default(defaultValues.one),
|
|
8783
|
+
two: z.string().default(defaultValues.two),
|
|
8784
|
+
})
|
|
8785
|
+
|
|
8786
|
+
export const Route = createFileRoute('/hello')({
|
|
8787
|
+
validateSearch: zodValidator(searchSchema),
|
|
8788
|
+
search: {
|
|
8789
|
+
// strip default values
|
|
8790
|
+
middlewares: [stripSearchParams(defaultValues)],
|
|
8791
|
+
},
|
|
8792
|
+
})
|
|
8793
|
+
\`\`\`
|
|
8794
|
+
|
|
8795
|
+
<!-- ::end:framework -->
|
|
8796
|
+
|
|
6719
8797
|
Multiple middlewares can be chained. The following example shows how to combine both \`retainSearchParams\` and \`stripSearchParams\`.
|
|
6720
8798
|
|
|
8799
|
+
<!-- ::start:framework -->
|
|
8800
|
+
|
|
8801
|
+
# React
|
|
8802
|
+
|
|
6721
8803
|
\`\`\`tsx
|
|
6722
8804
|
import {
|
|
6723
8805
|
Link,
|
|
@@ -6747,6 +8829,39 @@ export const Route = createFileRoute('/search')({
|
|
|
6747
8829
|
})
|
|
6748
8830
|
\`\`\`
|
|
6749
8831
|
|
|
8832
|
+
# Solid
|
|
8833
|
+
|
|
8834
|
+
\`\`\`tsx
|
|
8835
|
+
import {
|
|
8836
|
+
Link,
|
|
8837
|
+
createFileRoute,
|
|
8838
|
+
retainSearchParams,
|
|
8839
|
+
stripSearchParams,
|
|
8840
|
+
} from '@tanstack/solid-router'
|
|
8841
|
+
import { z } from 'zod'
|
|
8842
|
+
import { zodValidator } from '@tanstack/zod-adapter'
|
|
8843
|
+
|
|
8844
|
+
const defaultValues = ['foo', 'bar']
|
|
8845
|
+
|
|
8846
|
+
export const Route = createFileRoute('/search')({
|
|
8847
|
+
validateSearch: zodValidator(
|
|
8848
|
+
z.object({
|
|
8849
|
+
retainMe: z.string().optional(),
|
|
8850
|
+
arrayWithDefaults: z.string().array().default(defaultValues),
|
|
8851
|
+
required: z.string(),
|
|
8852
|
+
}),
|
|
8853
|
+
),
|
|
8854
|
+
search: {
|
|
8855
|
+
middlewares: [
|
|
8856
|
+
retainSearchParams(['retainMe']),
|
|
8857
|
+
stripSearchParams({ arrayWithDefaults: defaultValues }),
|
|
8858
|
+
],
|
|
8859
|
+
},
|
|
8860
|
+
})
|
|
8861
|
+
\`\`\`
|
|
8862
|
+
|
|
8863
|
+
<!-- ::end:framework -->
|
|
8864
|
+
|
|
6750
8865
|
# SSR
|
|
6751
8866
|
|
|
6752
8867
|
> [!WARNING]
|
|
@@ -6770,6 +8885,10 @@ Non-Streaming server-side rendering is the classic process of rendering the mark
|
|
|
6770
8885
|
|
|
6771
8886
|
To implement non-streaming SSR with TanStack Router, you will need the following utilities:
|
|
6772
8887
|
|
|
8888
|
+
<!-- ::start:framework -->
|
|
8889
|
+
|
|
8890
|
+
# React
|
|
8891
|
+
|
|
6773
8892
|
- \`RouterClient\` from \`@tanstack/react-router\`
|
|
6774
8893
|
- e.g. \`<RouterClient router={router} />\`
|
|
6775
8894
|
- Rendering this component in your client entry will render your application and also automatically implement the \`Wrap\` component option on \`Router\`
|
|
@@ -6782,6 +8901,22 @@ To implement non-streaming SSR with TanStack Router, you will need the following
|
|
|
6782
8901
|
- \`RouterServer\` from \`@tanstack/react-router\`
|
|
6783
8902
|
- This implements the \`Wrap\` component option on \`Router\`
|
|
6784
8903
|
|
|
8904
|
+
# Solid
|
|
8905
|
+
|
|
8906
|
+
- \`RouterClient\` from \`@tanstack/solid-router\`
|
|
8907
|
+
- e.g. \`<RouterClient router={router} />\`
|
|
8908
|
+
- Rendering this component in your client entry will render your application and also automatically implement the \`Wrap\` component option on \`Router\`
|
|
8909
|
+
- And, either:
|
|
8910
|
+
- \`defaultRenderHandler\` from \`@tanstack/solid-router\`
|
|
8911
|
+
- This will render your application in your server entry and also automatically handle application-level hydration/dehydration and also automatically implement the RouterServer component.
|
|
8912
|
+
or:
|
|
8913
|
+
- \`renderRouterToString\` from \`@tanstack/solid-router\`
|
|
8914
|
+
- This differs from defaultRenderHandler in that it allows you to manually specify the \`Wrap\` component option on \`Router\` together with any other providers you may need to wrap it with.
|
|
8915
|
+
- \`RouterServer\` from \`@tanstack/solid-router\`
|
|
8916
|
+
- This implements the \`Wrap\` component option on \`Router\`
|
|
8917
|
+
|
|
8918
|
+
<!-- ::end:framework -->
|
|
8919
|
+
|
|
6785
8920
|
### Automatic Server History
|
|
6786
8921
|
|
|
6787
8922
|
On the client, Router defaults to using an instance of \`createBrowserHistory\`, which is the preferred type of history to use on the client. On the server, however, you will want to use an instance of \`createMemoryHistory\` instead. This is because \`createBrowserHistory\` uses the \`window\` object, which does not exist on the server. This is handled automatically for you in the RouterServer component.
|
|
@@ -6798,8 +8933,11 @@ For more information on how to utilize data loading, see the [Data Loading](./da
|
|
|
6798
8933
|
|
|
6799
8934
|
Since your router will exist both on the server and the client, it's important that you create your router in a way that is consistent between both of these environments. The easiest way to do this is to expose a \`createRouter\` function in a shared file that can be imported and called by both your server and client entry files.
|
|
6800
8935
|
|
|
6801
|
-
|
|
6802
|
-
|
|
8936
|
+
<!-- ::start:framework -->
|
|
8937
|
+
|
|
8938
|
+
# React
|
|
8939
|
+
|
|
8940
|
+
\`\`\`tsx title='src/router.tsx'
|
|
6803
8941
|
import { createRouter as createTanstackRouter } from '@tanstack/react-router'
|
|
6804
8942
|
import { routeTree } from './routeTree.gen'
|
|
6805
8943
|
|
|
@@ -6814,14 +8952,36 @@ declare module '@tanstack/react-router' {
|
|
|
6814
8952
|
}
|
|
6815
8953
|
\`\`\`
|
|
6816
8954
|
|
|
8955
|
+
# Solid
|
|
8956
|
+
|
|
8957
|
+
\`\`\`tsx title='src/router.tsx'
|
|
8958
|
+
import { createRouter as createTanstackRouter } from '@tanstack/solid-router'
|
|
8959
|
+
import { routeTree } from './routeTree.gen'
|
|
8960
|
+
|
|
8961
|
+
export function createRouter() {
|
|
8962
|
+
return createTanstackRouter({ routeTree })
|
|
8963
|
+
}
|
|
8964
|
+
|
|
8965
|
+
declare module '@tanstack/solid-router' {
|
|
8966
|
+
interface Register {
|
|
8967
|
+
router: ReturnType<typeof createRouter>
|
|
8968
|
+
}
|
|
8969
|
+
}
|
|
8970
|
+
\`\`\`
|
|
8971
|
+
|
|
8972
|
+
<!-- ::end:framework -->
|
|
8973
|
+
|
|
6817
8974
|
### Rendering the Application on the Server
|
|
6818
8975
|
|
|
6819
8976
|
Now that you have a router instance that has loaded all the critical data for the current URL, you can render your application on the server:
|
|
6820
8977
|
|
|
6821
8978
|
using \`defaultRenderHandler\`
|
|
6822
8979
|
|
|
6823
|
-
|
|
6824
|
-
|
|
8980
|
+
<!-- ::start:framework -->
|
|
8981
|
+
|
|
8982
|
+
# React
|
|
8983
|
+
|
|
8984
|
+
\`\`\`tsx title="src/entry-server.tsx"
|
|
6825
8985
|
import {
|
|
6826
8986
|
createRequestHandler,
|
|
6827
8987
|
defaultRenderToString,
|
|
@@ -6835,10 +8995,55 @@ export async function render({ request }: { request: Request }) {
|
|
|
6835
8995
|
}
|
|
6836
8996
|
\`\`\`
|
|
6837
8997
|
|
|
8998
|
+
# Solid
|
|
8999
|
+
|
|
9000
|
+
\`\`\`tsx title="src/entry-server.tsx"
|
|
9001
|
+
import {
|
|
9002
|
+
createRequestHandler,
|
|
9003
|
+
defaultRenderToString,
|
|
9004
|
+
} from '@tanstack/solid-router/ssr/server'
|
|
9005
|
+
import { createRouter } from './router'
|
|
9006
|
+
|
|
9007
|
+
export async function render({ request }: { request: Request }) {
|
|
9008
|
+
const handler = createRequestHandler({ request, createRouter })
|
|
9009
|
+
|
|
9010
|
+
return await handler(defaultRenderHandler)
|
|
9011
|
+
}
|
|
9012
|
+
\`\`\`
|
|
9013
|
+
|
|
9014
|
+
<!-- ::end:framework -->
|
|
9015
|
+
|
|
6838
9016
|
using \`renderRouterToString\`
|
|
6839
9017
|
|
|
6840
|
-
|
|
6841
|
-
|
|
9018
|
+
<!-- ::start:framework -->
|
|
9019
|
+
|
|
9020
|
+
# React
|
|
9021
|
+
|
|
9022
|
+
\`\`\`tsx title="src/entry-server.tsx"
|
|
9023
|
+
import {
|
|
9024
|
+
createRequestHandler,
|
|
9025
|
+
renderRouterToString,
|
|
9026
|
+
RouterServer,
|
|
9027
|
+
} from '@tanstack/react-router/ssr/server'
|
|
9028
|
+
import { createRouter } from './router'
|
|
9029
|
+
|
|
9030
|
+
export function render({ request }: { request: Request }) {
|
|
9031
|
+
const handler = createRequestHandler({ request, createRouter })
|
|
9032
|
+
|
|
9033
|
+
return handler(({ request, responseHeaders, router }) =>
|
|
9034
|
+
renderRouterToString({
|
|
9035
|
+
request,
|
|
9036
|
+
responseHeaders,
|
|
9037
|
+
router,
|
|
9038
|
+
children: <RouterServer router={router} />,
|
|
9039
|
+
}),
|
|
9040
|
+
)
|
|
9041
|
+
}
|
|
9042
|
+
\`\`\`
|
|
9043
|
+
|
|
9044
|
+
# Solid
|
|
9045
|
+
|
|
9046
|
+
\`\`\`tsx title="src/entry-server.tsx"
|
|
6842
9047
|
import {
|
|
6843
9048
|
createRequestHandler,
|
|
6844
9049
|
renderRouterToString,
|
|
@@ -6860,6 +9065,8 @@ export function render({ request }: { request: Request }) {
|
|
|
6860
9065
|
}
|
|
6861
9066
|
\`\`\`
|
|
6862
9067
|
|
|
9068
|
+
<!-- ::end:framework -->
|
|
9069
|
+
|
|
6863
9070
|
NOTE: The createRequestHandler method requires a web api standard Request object, while the handler method will return a web api standard Response promise.
|
|
6864
9071
|
|
|
6865
9072
|
Should you be using a server framework like Express that uses its own Request and Response objects you would need to convert from the one to the other. Please have a look at the examples for how such an implementation might look like.
|
|
@@ -6871,10 +9078,11 @@ On the client, things are much simpler.
|
|
|
6871
9078
|
- Create your router instance
|
|
6872
9079
|
- Render your application using the \`<RouterClient />\` component
|
|
6873
9080
|
|
|
6874
|
-
|
|
9081
|
+
<!-- ::start:framework -->
|
|
6875
9082
|
|
|
6876
|
-
|
|
6877
|
-
|
|
9083
|
+
# React
|
|
9084
|
+
|
|
9085
|
+
\`\`\`tsx title="src/entry-client.tsx"
|
|
6878
9086
|
import { hydrateRoot } from 'react-dom/client'
|
|
6879
9087
|
import { RouterClient } from '@tanstack/react-router/ssr/client'
|
|
6880
9088
|
import { createRouter } from './router'
|
|
@@ -6884,45 +9092,105 @@ const router = createRouter()
|
|
|
6884
9092
|
hydrateRoot(document, <RouterClient router={router} />)
|
|
6885
9093
|
\`\`\`
|
|
6886
9094
|
|
|
6887
|
-
|
|
9095
|
+
# Solid
|
|
9096
|
+
|
|
9097
|
+
\`\`\`tsx title="src/entry-client.tsx"
|
|
9098
|
+
import { hydrate } from 'solid-js/web'
|
|
9099
|
+
import { RouterClient } from '@tanstack/solid-router/ssr/client'
|
|
9100
|
+
import { createRouter } from './router'
|
|
9101
|
+
|
|
9102
|
+
const router = createRouter()
|
|
9103
|
+
|
|
9104
|
+
hydrate(() => <RouterClient router={router} />, document.body)
|
|
9105
|
+
\`\`\`
|
|
9106
|
+
|
|
9107
|
+
<!-- ::end:framework -->
|
|
9108
|
+
|
|
9109
|
+
With this setup, your application will be rendered on the server and then hydrated on the client!
|
|
9110
|
+
|
|
9111
|
+
## Streaming SSR
|
|
9112
|
+
|
|
9113
|
+
Streaming SSR is the most modern flavor of SSR and is the process of continuously and incrementally sending HTML markup to the client as it is rendered on the server. This is slightly different from traditional SSR in concept because beyond being able to dehydrate and rehydrate a critical first paint, markup and data with lower priority or slower response times can be streamed to the client after the initial render, but in the same request.
|
|
9114
|
+
|
|
9115
|
+
This pattern can be useful for pages that have slow or high-latency data fetching requirements. For example, if you have a page that needs to fetch data from a third-party API, you can stream the critical initial markup and data to the client and then stream the less-critical third-party data to the client as it is resolved.
|
|
9116
|
+
|
|
9117
|
+
> [!NOTE]
|
|
9118
|
+
> This streaming pattern is all automatic as long as you are using either \`defaultStreamHandler\` or \`renderRouterToStream\`.
|
|
9119
|
+
|
|
9120
|
+
using \`defaultStreamHandler\`
|
|
9121
|
+
|
|
9122
|
+
<!-- ::start:framework -->
|
|
9123
|
+
|
|
9124
|
+
# React
|
|
9125
|
+
|
|
9126
|
+
\`\`\`tsx title="src/entry-server.tsx"
|
|
9127
|
+
import {
|
|
9128
|
+
createRequestHandler,
|
|
9129
|
+
defaultStreamHandler,
|
|
9130
|
+
} from '@tanstack/react-router/ssr/server'
|
|
9131
|
+
import { createRouter } from './router'
|
|
9132
|
+
|
|
9133
|
+
export async function render({ request }: { request: Request }) {
|
|
9134
|
+
const handler = createRequestHandler({ request, createRouter })
|
|
9135
|
+
|
|
9136
|
+
return await handler(defaultStreamHandler)
|
|
9137
|
+
}
|
|
9138
|
+
\`\`\`
|
|
9139
|
+
|
|
9140
|
+
# Solid
|
|
6888
9141
|
|
|
6889
|
-
|
|
9142
|
+
\`\`\`tsx title="src/entry-server.tsx"
|
|
9143
|
+
import {
|
|
9144
|
+
createRequestHandler,
|
|
9145
|
+
defaultStreamHandler,
|
|
9146
|
+
} from '@tanstack/solid-router/ssr/server'
|
|
9147
|
+
import { createRouter } from './router'
|
|
6890
9148
|
|
|
6891
|
-
|
|
9149
|
+
export async function render({ request }: { request: Request }) {
|
|
9150
|
+
const handler = createRequestHandler({ request, createRouter })
|
|
6892
9151
|
|
|
6893
|
-
|
|
9152
|
+
return await handler(defaultStreamHandler)
|
|
9153
|
+
}
|
|
9154
|
+
\`\`\`
|
|
6894
9155
|
|
|
6895
|
-
|
|
9156
|
+
<!-- ::end:framework -->
|
|
6896
9157
|
|
|
6897
|
-
|
|
6898
|
-
> This streaming pattern is all automatic as long as you are using either \`defaultStreamHandler\` or \`renderRouterToStream\`.
|
|
9158
|
+
using \`renderRouterToStream\`
|
|
6899
9159
|
|
|
6900
|
-
|
|
9160
|
+
<!-- ::start:framework -->
|
|
6901
9161
|
|
|
6902
|
-
|
|
6903
|
-
|
|
9162
|
+
# React
|
|
9163
|
+
|
|
9164
|
+
\`\`\`tsx title="src/entry-server.tsx"
|
|
6904
9165
|
import {
|
|
6905
9166
|
createRequestHandler,
|
|
6906
|
-
|
|
9167
|
+
renderRouterToStream,
|
|
9168
|
+
RouterServer,
|
|
6907
9169
|
} from '@tanstack/react-router/ssr/server'
|
|
6908
9170
|
import { createRouter } from './router'
|
|
6909
9171
|
|
|
6910
|
-
export
|
|
9172
|
+
export function render({ request }: { request: Request }) {
|
|
6911
9173
|
const handler = createRequestHandler({ request, createRouter })
|
|
6912
9174
|
|
|
6913
|
-
return
|
|
9175
|
+
return handler(({ request, responseHeaders, router }) =>
|
|
9176
|
+
renderRouterToStream({
|
|
9177
|
+
request,
|
|
9178
|
+
responseHeaders,
|
|
9179
|
+
router,
|
|
9180
|
+
children: <RouterServer router={router} />,
|
|
9181
|
+
}),
|
|
9182
|
+
)
|
|
6914
9183
|
}
|
|
6915
9184
|
\`\`\`
|
|
6916
9185
|
|
|
6917
|
-
|
|
9186
|
+
# Solid
|
|
6918
9187
|
|
|
6919
|
-
\`\`\`tsx
|
|
6920
|
-
// src/entry-server.tsx
|
|
9188
|
+
\`\`\`tsx title="src/entry-server.tsx"
|
|
6921
9189
|
import {
|
|
6922
9190
|
createRequestHandler,
|
|
6923
9191
|
renderRouterToStream,
|
|
6924
9192
|
RouterServer,
|
|
6925
|
-
} from '@tanstack/
|
|
9193
|
+
} from '@tanstack/solid-router/ssr/server'
|
|
6926
9194
|
import { createRouter } from './router'
|
|
6927
9195
|
|
|
6928
9196
|
export function render({ request }: { request: Request }) {
|
|
@@ -6939,6 +9207,8 @@ export function render({ request }: { request: Request }) {
|
|
|
6939
9207
|
}
|
|
6940
9208
|
\`\`\`
|
|
6941
9209
|
|
|
9210
|
+
<!-- ::end:framework -->
|
|
9211
|
+
|
|
6942
9212
|
## Streaming Dehydration/Hydration
|
|
6943
9213
|
|
|
6944
9214
|
Streaming dehydration/hydration is an advanced pattern that goes beyond markup and allows you to dehydrate and stream any supporting data from the server to the client and rehydrate it on arrival. This is useful for applications that may need to further use/manage the underlying data that was used to render the initial markup on the server.
|
|
@@ -6966,9 +9236,13 @@ In addition to being able to access this data from the route itself, you can als
|
|
|
6966
9236
|
|
|
6967
9237
|
## Example
|
|
6968
9238
|
|
|
6969
|
-
|
|
9239
|
+
<!-- ::start:framework -->
|
|
6970
9240
|
|
|
6971
|
-
|
|
9241
|
+
# React
|
|
9242
|
+
|
|
9243
|
+
<!-- ::start:tabs variant="files" -->
|
|
9244
|
+
|
|
9245
|
+
\`\`\`tsx title='src/routes/posts.tsx'
|
|
6972
9246
|
import { createFileRoute } from '@tanstack/react-router'
|
|
6973
9247
|
|
|
6974
9248
|
export const Route = createFileRoute('/posts')({
|
|
@@ -6978,11 +9252,13 @@ export const Route = createFileRoute('/posts')({
|
|
|
6978
9252
|
})
|
|
6979
9253
|
\`\`\`
|
|
6980
9254
|
|
|
9255
|
+
<!-- ::end:tabs -->
|
|
9256
|
+
|
|
6981
9257
|
You can then access this data anywhere you have access to your routes, including matches that can be mapped back to their routes.
|
|
6982
9258
|
|
|
6983
|
-
|
|
9259
|
+
<!-- ::start:tabs variant="files" -->
|
|
6984
9260
|
|
|
6985
|
-
\`\`\`tsx
|
|
9261
|
+
\`\`\`tsx title='src/routes/__root.tsx'
|
|
6986
9262
|
import { createRootRoute } from '@tanstack/react-router'
|
|
6987
9263
|
|
|
6988
9264
|
export const Route = createRootRoute({
|
|
@@ -7000,10 +9276,59 @@ export const Route = createRootRoute({
|
|
|
7000
9276
|
})
|
|
7001
9277
|
\`\`\`
|
|
7002
9278
|
|
|
9279
|
+
<!-- ::end:tabs -->
|
|
9280
|
+
|
|
9281
|
+
# Solid
|
|
9282
|
+
|
|
9283
|
+
<!-- ::start:tabs variant="files" -->
|
|
9284
|
+
|
|
9285
|
+
\`\`\`tsx title='src/routes/posts.tsx'
|
|
9286
|
+
import { createFileRoute } from '@tanstack/solid-router'
|
|
9287
|
+
|
|
9288
|
+
export const Route = createFileRoute('/posts')({
|
|
9289
|
+
staticData: {
|
|
9290
|
+
customData: 'Hello!',
|
|
9291
|
+
},
|
|
9292
|
+
})
|
|
9293
|
+
\`\`\`
|
|
9294
|
+
|
|
9295
|
+
<!-- ::end:tabs -->
|
|
9296
|
+
|
|
9297
|
+
You can then access this data anywhere you have access to your routes, including matches that can be mapped back to their routes.
|
|
9298
|
+
|
|
9299
|
+
<!-- ::start:tabs variant="files" -->
|
|
9300
|
+
|
|
9301
|
+
\`\`\`tsx title='src/routes/__root.tsx'
|
|
9302
|
+
import { createRootRoute, useMatches } from '@tanstack/solid-router'
|
|
9303
|
+
import { For } from 'solid-js'
|
|
9304
|
+
|
|
9305
|
+
export const Route = createRootRoute({
|
|
9306
|
+
component: () => {
|
|
9307
|
+
const matches = useMatches()
|
|
9308
|
+
|
|
9309
|
+
return (
|
|
9310
|
+
<div>
|
|
9311
|
+
<For each={matches()}>
|
|
9312
|
+
{(match) => <div>{match.staticData.customData}</div>}
|
|
9313
|
+
</For>
|
|
9314
|
+
</div>
|
|
9315
|
+
)
|
|
9316
|
+
},
|
|
9317
|
+
})
|
|
9318
|
+
\`\`\`
|
|
9319
|
+
|
|
9320
|
+
<!-- ::end:tabs -->
|
|
9321
|
+
|
|
9322
|
+
<!-- ::end:framework -->
|
|
9323
|
+
|
|
7003
9324
|
## Enforcing Static Data
|
|
7004
9325
|
|
|
7005
9326
|
If you want to enforce that a route has static data, you can use declaration merging to add a type to the route's static option:
|
|
7006
9327
|
|
|
9328
|
+
<!-- ::start:framework -->
|
|
9329
|
+
|
|
9330
|
+
# React
|
|
9331
|
+
|
|
7007
9332
|
\`\`\`tsx
|
|
7008
9333
|
declare module '@tanstack/react-router' {
|
|
7009
9334
|
interface StaticDataRouteOption {
|
|
@@ -7012,11 +9337,21 @@ declare module '@tanstack/react-router' {
|
|
|
7012
9337
|
}
|
|
7013
9338
|
\`\`\`
|
|
7014
9339
|
|
|
7015
|
-
|
|
9340
|
+
# Solid
|
|
7016
9341
|
|
|
7017
9342
|
\`\`\`tsx
|
|
7018
|
-
|
|
9343
|
+
declare module '@tanstack/solid-router' {
|
|
9344
|
+
interface StaticDataRouteOption {
|
|
9345
|
+
customData: string
|
|
9346
|
+
}
|
|
9347
|
+
}
|
|
9348
|
+
\`\`\`
|
|
9349
|
+
|
|
9350
|
+
<!-- ::end:framework -->
|
|
7019
9351
|
|
|
9352
|
+
Now, if you try to create a route without the \`customData\` property, you'll get a type error:
|
|
9353
|
+
|
|
9354
|
+
\`\`\`tsx
|
|
7020
9355
|
export const Route = createFileRoute('/posts')({
|
|
7021
9356
|
staticData: {
|
|
7022
9357
|
// Property 'customData' is missing in type '{ customData: number; }' but required in type 'StaticDataRouteOption'.ts(2741)
|
|
@@ -7028,6 +9363,10 @@ export const Route = createFileRoute('/posts')({
|
|
|
7028
9363
|
|
|
7029
9364
|
If you want to make static data optional, simply add a \`?\` to the property:
|
|
7030
9365
|
|
|
9366
|
+
<!-- ::start:framework -->
|
|
9367
|
+
|
|
9368
|
+
# React
|
|
9369
|
+
|
|
7031
9370
|
\`\`\`tsx
|
|
7032
9371
|
declare module '@tanstack/react-router' {
|
|
7033
9372
|
interface StaticDataRouteOption {
|
|
@@ -7036,6 +9375,18 @@ declare module '@tanstack/react-router' {
|
|
|
7036
9375
|
}
|
|
7037
9376
|
\`\`\`
|
|
7038
9377
|
|
|
9378
|
+
# Solid
|
|
9379
|
+
|
|
9380
|
+
\`\`\`tsx
|
|
9381
|
+
declare module '@tanstack/solid-router' {
|
|
9382
|
+
interface StaticDataRouteOption {
|
|
9383
|
+
customData?: string
|
|
9384
|
+
}
|
|
9385
|
+
}
|
|
9386
|
+
\`\`\`
|
|
9387
|
+
|
|
9388
|
+
<!-- ::end:framework -->
|
|
9389
|
+
|
|
7039
9390
|
As long as there are any required properties on the \`StaticDataRouteOption\`, you'll be required to pass in an object.
|
|
7040
9391
|
|
|
7041
9392
|
## Common Patterns
|
|
@@ -7044,14 +9395,19 @@ As long as there are any required properties on the \`StaticDataRouteOption\`, y
|
|
|
7044
9395
|
|
|
7045
9396
|
Use staticData to control which routes show or hide layout elements:
|
|
7046
9397
|
|
|
7047
|
-
|
|
7048
|
-
|
|
9398
|
+
<!-- ::start:tabs variant="files" -->
|
|
9399
|
+
|
|
9400
|
+
\`\`\`tsx title='src/routes/admin/route.tsx'
|
|
7049
9401
|
export const Route = createFileRoute('/admin')({
|
|
7050
9402
|
staticData: { showNavbar: false },
|
|
7051
9403
|
component: AdminLayout,
|
|
7052
9404
|
})
|
|
7053
9405
|
\`\`\`
|
|
7054
9406
|
|
|
9407
|
+
<!-- ::end:tabs -->
|
|
9408
|
+
|
|
9409
|
+
<!-- ::start:tabs variant="files" -->
|
|
9410
|
+
|
|
7055
9411
|
\`\`\`tsx
|
|
7056
9412
|
// routes/__root.tsx
|
|
7057
9413
|
function RootComponent() {
|
|
@@ -7070,10 +9426,17 @@ function RootComponent() {
|
|
|
7070
9426
|
}
|
|
7071
9427
|
\`\`\`
|
|
7072
9428
|
|
|
9429
|
+
<!-- ::end:tabs -->
|
|
9430
|
+
|
|
7073
9431
|
### Route Titles for Breadcrumbs
|
|
7074
9432
|
|
|
7075
|
-
|
|
7076
|
-
|
|
9433
|
+
<!-- ::start:framework -->
|
|
9434
|
+
|
|
9435
|
+
# React
|
|
9436
|
+
|
|
9437
|
+
<!-- ::start:tabs variant="files" -->
|
|
9438
|
+
|
|
9439
|
+
\`\`\`tsx title='src/routes/posts/$postId.tsx'
|
|
7077
9440
|
export const Route = createFileRoute('/posts/$postId')({
|
|
7078
9441
|
staticData: {
|
|
7079
9442
|
getTitle: () => 'Post Details',
|
|
@@ -7081,8 +9444,11 @@ export const Route = createFileRoute('/posts/$postId')({
|
|
|
7081
9444
|
})
|
|
7082
9445
|
\`\`\`
|
|
7083
9446
|
|
|
7084
|
-
|
|
7085
|
-
|
|
9447
|
+
<!-- ::end:tabs -->
|
|
9448
|
+
|
|
9449
|
+
<!-- ::start:tabs variant="files" -->
|
|
9450
|
+
|
|
9451
|
+
\`\`\`tsx title='src/components/Breadcrumbs.tsx'
|
|
7086
9452
|
function Breadcrumbs() {
|
|
7087
9453
|
const matches = useMatches()
|
|
7088
9454
|
|
|
@@ -7098,6 +9464,45 @@ function Breadcrumbs() {
|
|
|
7098
9464
|
}
|
|
7099
9465
|
\`\`\`
|
|
7100
9466
|
|
|
9467
|
+
<!-- ::end:tabs -->
|
|
9468
|
+
|
|
9469
|
+
# Solid
|
|
9470
|
+
|
|
9471
|
+
<!-- ::start:tabs variant="files" -->
|
|
9472
|
+
|
|
9473
|
+
\`\`\`tsx title='src/routes/posts/$postId.tsx'
|
|
9474
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
9475
|
+
staticData: {
|
|
9476
|
+
getTitle: () => 'Post Details',
|
|
9477
|
+
},
|
|
9478
|
+
})
|
|
9479
|
+
\`\`\`
|
|
9480
|
+
|
|
9481
|
+
<!-- ::end:tabs -->
|
|
9482
|
+
|
|
9483
|
+
<!-- ::start:tabs variant="files" -->
|
|
9484
|
+
|
|
9485
|
+
\`\`\`tsx title='src/components/Breadcrumbs.tsx'
|
|
9486
|
+
import { useMatches } from '@tanstack/solid-router'
|
|
9487
|
+
import { For } from 'solid-js'
|
|
9488
|
+
|
|
9489
|
+
function Breadcrumbs() {
|
|
9490
|
+
const matches = useMatches()
|
|
9491
|
+
|
|
9492
|
+
return (
|
|
9493
|
+
<nav>
|
|
9494
|
+
<For each={matches().filter((m) => m.staticData?.getTitle)}>
|
|
9495
|
+
{(m) => <span>{m.staticData.getTitle()}</span>}
|
|
9496
|
+
</For>
|
|
9497
|
+
</nav>
|
|
9498
|
+
)
|
|
9499
|
+
}
|
|
9500
|
+
\`\`\`
|
|
9501
|
+
|
|
9502
|
+
<!-- ::end:tabs -->
|
|
9503
|
+
|
|
9504
|
+
<!-- ::end:framework -->
|
|
9505
|
+
|
|
7101
9506
|
### When to Use staticData vs Context
|
|
7102
9507
|
|
|
7103
9508
|
| staticData | context |
|
|
@@ -7136,6 +9541,10 @@ const parentRoute = createRoute({
|
|
|
7136
9541
|
|
|
7137
9542
|
For the types of your router to work with top-level exports like \`Link\`, \`useNavigate\`, \`useParams\`, etc. they must permeate the TypeScript module boundary and be registered right into the library. To do this, we use declaration merging on the exported \`Register\` interface.
|
|
7138
9543
|
|
|
9544
|
+
<!-- ::start:framework -->
|
|
9545
|
+
|
|
9546
|
+
# React
|
|
9547
|
+
|
|
7139
9548
|
\`\`\`ts
|
|
7140
9549
|
const router = createRouter({
|
|
7141
9550
|
// ...
|
|
@@ -7148,6 +9557,22 @@ declare module '@tanstack/react-router' {
|
|
|
7148
9557
|
}
|
|
7149
9558
|
\`\`\`
|
|
7150
9559
|
|
|
9560
|
+
# Solid
|
|
9561
|
+
|
|
9562
|
+
\`\`\`ts
|
|
9563
|
+
const router = createRouter({
|
|
9564
|
+
// ...
|
|
9565
|
+
})
|
|
9566
|
+
|
|
9567
|
+
declare module '@tanstack/solid-router' {
|
|
9568
|
+
interface Register {
|
|
9569
|
+
router: typeof router
|
|
9570
|
+
}
|
|
9571
|
+
}
|
|
9572
|
+
\`\`\`
|
|
9573
|
+
|
|
9574
|
+
<!-- ::end:framework -->
|
|
9575
|
+
|
|
7151
9576
|
By registering your router with the module, you can now use the exported hooks, components, and utilities with your router's exact types.
|
|
7152
9577
|
|
|
7153
9578
|
## Fixing the Component Context Problem
|
|
@@ -7402,6 +9827,10 @@ Most types exposed by TanStack Router are internal, subject to breaking changes
|
|
|
7402
9827
|
|
|
7403
9828
|
\`ValidateLinkOptions\` type checks object literal types to ensure they conform to \`Link\` options at inference sites. For example, you may have a generic \`HeadingLink\` component which accepts a \`title\` prop along with \`linkOptions\`, the idea being this component can be re-used for any navigation.
|
|
7404
9829
|
|
|
9830
|
+
<!-- ::start:framework -->
|
|
9831
|
+
|
|
9832
|
+
# React
|
|
9833
|
+
|
|
7405
9834
|
\`\`\`tsx
|
|
7406
9835
|
export interface HeaderLinkProps<
|
|
7407
9836
|
TRouter extends RegisteredRouter = RegisteredRouter,
|
|
@@ -7424,6 +9853,32 @@ export function HeadingLink(props: HeaderLinkProps): React.ReactNode {
|
|
|
7424
9853
|
}
|
|
7425
9854
|
\`\`\`
|
|
7426
9855
|
|
|
9856
|
+
# Solid
|
|
9857
|
+
|
|
9858
|
+
\`\`\`tsx
|
|
9859
|
+
export interface HeaderLinkProps<
|
|
9860
|
+
TRouter extends RegisteredRouter = RegisteredRouter,
|
|
9861
|
+
TOptions = unknown,
|
|
9862
|
+
> {
|
|
9863
|
+
title: string
|
|
9864
|
+
linkOptions: ValidateLinkOptions<TRouter, TOptions>
|
|
9865
|
+
}
|
|
9866
|
+
|
|
9867
|
+
export function HeadingLink<TRouter extends RegisteredRouter, TOptions>(
|
|
9868
|
+
props: HeaderLinkProps<TRouter, TOptions>,
|
|
9869
|
+
): Solid.JSX.Element
|
|
9870
|
+
export function HeadingLink(props: HeaderLinkProps): Solid.JSX.Element {
|
|
9871
|
+
return (
|
|
9872
|
+
<>
|
|
9873
|
+
<h1>{props.title}</h1>
|
|
9874
|
+
<Link {...props.linkOptions} />
|
|
9875
|
+
</>
|
|
9876
|
+
)
|
|
9877
|
+
}
|
|
9878
|
+
\`\`\`
|
|
9879
|
+
|
|
9880
|
+
<!-- ::end:framework -->
|
|
9881
|
+
|
|
7427
9882
|
A more permissive overload of \`HeadingLink\` is used to avoid type assertions you would otherwise have to do with the generic signature. Using a looser signature without type parameters is an easy way to avoid type assertions in the implementation of \`HeadingLink\`
|
|
7428
9883
|
|
|
7429
9884
|
All type parameters for utilities are optional but for the best TypeScript performance \`TRouter\` should always be specified for the public facing signature. And \`TOptions\` should always be used at inference sites like \`HeadingLink\` to infer the \`linkOptions\` to correctly narrow \`params\` and \`search\`
|
|
@@ -7439,6 +9894,10 @@ The result of this is that \`linkOptions\` in the following is completely type-s
|
|
|
7439
9894
|
|
|
7440
9895
|
All navigation type utilities have an array variant. \`ValidateLinkOptionsArray\` enables type checking of an array of \`Link\` options. For example, you might have a generic \`Menu\` component where each item is a \`Link\`.
|
|
7441
9896
|
|
|
9897
|
+
<!-- ::start:framework -->
|
|
9898
|
+
|
|
9899
|
+
# React
|
|
9900
|
+
|
|
7442
9901
|
\`\`\`tsx
|
|
7443
9902
|
export interface MenuProps<
|
|
7444
9903
|
TRouter extends RegisteredRouter = RegisteredRouter,
|
|
@@ -7464,6 +9923,39 @@ export function Menu(props: MenuProps): React.ReactNode {
|
|
|
7464
9923
|
}
|
|
7465
9924
|
\`\`\`
|
|
7466
9925
|
|
|
9926
|
+
# Solid
|
|
9927
|
+
|
|
9928
|
+
\`\`\`tsx
|
|
9929
|
+
import { For } from 'solid-js'
|
|
9930
|
+
|
|
9931
|
+
export interface MenuProps<
|
|
9932
|
+
TRouter extends RegisteredRouter = RegisteredRouter,
|
|
9933
|
+
TItems extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
|
|
9934
|
+
> {
|
|
9935
|
+
items: ValidateLinkOptionsArray<TRouter, TItems>
|
|
9936
|
+
}
|
|
9937
|
+
|
|
9938
|
+
export function Menu<
|
|
9939
|
+
TRouter extends RegisteredRouter = RegisteredRouter,
|
|
9940
|
+
TItems extends ReadonlyArray<unknown>,
|
|
9941
|
+
>(props: MenuProps<TRouter, TItems>): Solid.JSX.Element
|
|
9942
|
+
export function Menu(props: MenuProps): Solid.JSX.Element {
|
|
9943
|
+
return (
|
|
9944
|
+
<ul>
|
|
9945
|
+
<For each={props.items}>
|
|
9946
|
+
{(item) => (
|
|
9947
|
+
<li>
|
|
9948
|
+
<Link {...item} />
|
|
9949
|
+
</li>
|
|
9950
|
+
)}
|
|
9951
|
+
</For>
|
|
9952
|
+
</ul>
|
|
9953
|
+
)
|
|
9954
|
+
}
|
|
9955
|
+
\`\`\`
|
|
9956
|
+
|
|
9957
|
+
<!-- ::end:framework -->
|
|
9958
|
+
|
|
7467
9959
|
This of course allows the following \`items\` prop to be completely type-safe
|
|
7468
9960
|
|
|
7469
9961
|
\`\`\`tsx
|
|
@@ -7477,7 +9969,11 @@ This of course allows the following \`items\` prop to be completely type-safe
|
|
|
7477
9969
|
|
|
7478
9970
|
It is also possible to fix \`from\` for each \`Link\` options in the array. This would allow all \`Menu\` items to navigate relative to \`from\`. Additional type checking of \`from\` can be provided by the \`ValidateFromPath\` utility
|
|
7479
9971
|
|
|
7480
|
-
|
|
9972
|
+
<!-- ::start:framework -->
|
|
9973
|
+
|
|
9974
|
+
# React
|
|
9975
|
+
|
|
9976
|
+
\`\`\`ts
|
|
7481
9977
|
export interface MenuProps<
|
|
7482
9978
|
TRouter extends RegisteredRouter = RegisteredRouter,
|
|
7483
9979
|
TItems extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
|
|
@@ -7505,6 +10001,42 @@ export function Menu(props: MenuProps): React.ReactNode {
|
|
|
7505
10001
|
}
|
|
7506
10002
|
\`\`\`
|
|
7507
10003
|
|
|
10004
|
+
# Solid
|
|
10005
|
+
|
|
10006
|
+
\`\`\`ts
|
|
10007
|
+
import { For } from 'solid-js'
|
|
10008
|
+
|
|
10009
|
+
export interface MenuProps<
|
|
10010
|
+
TRouter extends RegisteredRouter = RegisteredRouter,
|
|
10011
|
+
TItems extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
|
|
10012
|
+
TFrom extends string = string,
|
|
10013
|
+
> {
|
|
10014
|
+
from: ValidateFromPath<TRouter, TFrom>
|
|
10015
|
+
items: ValidateLinkOptionsArray<TRouter, TItems, TFrom>
|
|
10016
|
+
}
|
|
10017
|
+
|
|
10018
|
+
export function Menu<
|
|
10019
|
+
TRouter extends RegisteredRouter = RegisteredRouter,
|
|
10020
|
+
TItems extends ReadonlyArray<unknown>,
|
|
10021
|
+
TFrom extends string = string,
|
|
10022
|
+
>(props: MenuProps<TRouter, TItems, TFrom>): Solid.JSX.Element
|
|
10023
|
+
export function Menu(props: MenuProps): Solid.JSX.Element {
|
|
10024
|
+
return (
|
|
10025
|
+
<ul>
|
|
10026
|
+
<For each={props.items}>
|
|
10027
|
+
{(item) => (
|
|
10028
|
+
<li>
|
|
10029
|
+
<Link {...item} from={props.from} />
|
|
10030
|
+
</li>
|
|
10031
|
+
)}
|
|
10032
|
+
</For>
|
|
10033
|
+
</ul>
|
|
10034
|
+
)
|
|
10035
|
+
}
|
|
10036
|
+
\`\`\`
|
|
10037
|
+
|
|
10038
|
+
<!-- ::end:framework -->
|
|
10039
|
+
|
|
7508
10040
|
\`ValidateLinkOptionsArray\` allows you to fix \`from\` by providing an extra type parameter. The result is a type safe array of \`Link\` options providing navigation relative to \`from\`
|
|
7509
10041
|
|
|
7510
10042
|
\`\`\`tsx
|
|
@@ -7550,7 +10082,9 @@ fetchOrRedirect('http://example.com/', { to: '/login' })
|
|
|
7550
10082
|
|
|
7551
10083
|
\`ValidateNavigateOptions\` type checks object literal types to ensure they conform to navigate options at inference sites. For example, you may want to write a custom hook to enable/disable navigation.
|
|
7552
10084
|
|
|
7553
|
-
|
|
10085
|
+
<!-- ::start:framework -->
|
|
10086
|
+
|
|
10087
|
+
# React
|
|
7554
10088
|
|
|
7555
10089
|
\`\`\`tsx
|
|
7556
10090
|
export interface UseConditionalNavigateResult {
|
|
@@ -7582,7 +10116,41 @@ export function useConditionalNavigate(
|
|
|
7582
10116
|
}
|
|
7583
10117
|
\`\`\`
|
|
7584
10118
|
|
|
7585
|
-
|
|
10119
|
+
# Solid
|
|
10120
|
+
|
|
10121
|
+
\`\`\`tsx
|
|
10122
|
+
import { createSignal } from 'solid-js'
|
|
10123
|
+
|
|
10124
|
+
export interface UseConditionalNavigateResult {
|
|
10125
|
+
enable: () => void
|
|
10126
|
+
disable: () => void
|
|
10127
|
+
navigate: () => void
|
|
10128
|
+
}
|
|
10129
|
+
|
|
10130
|
+
export function useConditionalNavigate<
|
|
10131
|
+
TRouter extends RegisteredRouter = RegisteredRouter,
|
|
10132
|
+
TOptions = unknown,
|
|
10133
|
+
>(
|
|
10134
|
+
navigateOptions: ValidateNavigateOptions<TRouter, TOptions>,
|
|
10135
|
+
): UseConditionalNavigateResult
|
|
10136
|
+
export function useConditionalNavigate(
|
|
10137
|
+
navigateOptions: ValidateNavigateOptions,
|
|
10138
|
+
): UseConditionalNavigateResult {
|
|
10139
|
+
const [enabled, setEnabled] = createSignal(false)
|
|
10140
|
+
const navigate = useNavigate()
|
|
10141
|
+
return {
|
|
10142
|
+
enable: () => setEnabled(true),
|
|
10143
|
+
disable: () => setEnabled(false),
|
|
10144
|
+
navigate: () => {
|
|
10145
|
+
if (enabled()) {
|
|
10146
|
+
navigate(navigateOptions)
|
|
10147
|
+
}
|
|
10148
|
+
},
|
|
10149
|
+
}
|
|
10150
|
+
}
|
|
10151
|
+
\`\`\`
|
|
10152
|
+
|
|
10153
|
+
<!-- ::end:framework -->
|
|
7586
10154
|
|
|
7587
10155
|
The result of this is that \`navigateOptions\` passed to \`useConditionalNavigate\` is completely type-safe and we can enable/disable navigation based on react state
|
|
7588
10156
|
|