kaddidlehopper 0.1.0
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/CONTEXT.md +139 -0
- package/README.md +47 -0
- package/add-ons/ai/README.md +34 -0
- package/add-ons/ai/assets/_dot_env.local.append +13 -0
- package/add-ons/ai/assets/src/components/AIAssistant.tsx +149 -0
- package/add-ons/ai/assets/src/lib/ai-hook.ts +21 -0
- package/add-ons/ai/assets/src/lib/weather-tools.ts +30 -0
- package/add-ons/ai/assets/src/routes/api.chat.ts +94 -0
- package/add-ons/ai/assets/src/routes/chat.css +175 -0
- package/add-ons/ai/assets/src/routes/chat.tsx +141 -0
- package/add-ons/ai/info.json +27 -0
- package/add-ons/ai/package.json +17 -0
- package/add-ons/ai/small-logo.svg +8 -0
- package/dist/cli.js +251 -0
- package/dist/index.js +33 -0
- package/dist/types/cli.d.ts +8 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/types.d.ts +14 -0
- package/dist/types.js +1 -0
- package/examples/blog/README.md +60 -0
- package/examples/blog/assets/content/posts/beach.md +12 -0
- package/examples/blog/assets/content/posts/jungle.md.ejs +12 -0
- package/examples/blog/assets/content/posts/mountains.md.ejs +12 -0
- package/examples/blog/assets/content/posts/snorkeling.md.ejs +12 -0
- package/examples/blog/assets/content/posts/waterfall.md.ejs +12 -0
- package/examples/blog/assets/content-collections.ts +30 -0
- package/examples/blog/assets/public/beach.jpg +0 -0
- package/examples/blog/assets/public/jungle.jpg +0 -0
- package/examples/blog/assets/public/mountains.jpg +0 -0
- package/examples/blog/assets/public/snorkeling.jpg +0 -0
- package/examples/blog/assets/public/waterfall.jpg +0 -0
- package/examples/blog/assets/src/components/Header.tsx +52 -0
- package/examples/blog/assets/src/components/VacayAssistant.tsx +205 -0
- package/examples/blog/assets/src/components/blog-posts.tsx +78 -0
- package/examples/blog/assets/src/components/ui/card.tsx +92 -0
- package/examples/blog/assets/src/lib/blog-ai-hook.ts +25 -0
- package/examples/blog/assets/src/lib/blog-tools.ts +111 -0
- package/examples/blog/assets/src/lib/utils.ts +6 -0
- package/examples/blog/assets/src/routes/__root.tsx +57 -0
- package/examples/blog/assets/src/routes/api.blog-chat.ts +117 -0
- package/examples/blog/assets/src/routes/category.$category.tsx +19 -0
- package/examples/blog/assets/src/routes/index.tsx +19 -0
- package/examples/blog/assets/src/routes/posts.$slug.tsx +63 -0
- package/examples/blog/assets/src/styles.css +138 -0
- package/examples/blog/info.json +43 -0
- package/examples/blog/package.json +23 -0
- package/examples/events/README.md +110 -0
- package/examples/events/assets/content/speakers/andre-costa.md +22 -0
- package/examples/events/assets/content/speakers/hans-mueller.md.ejs +22 -0
- package/examples/events/assets/content/speakers/isabella-martinez.md.ejs +22 -0
- package/examples/events/assets/content/speakers/kenji-nakamura.md.ejs +22 -0
- package/examples/events/assets/content/speakers/marie-dubois.md.ejs +20 -0
- package/examples/events/assets/content/speakers/priya-sharma.md.ejs +22 -0
- package/examples/events/assets/content/talks/croissant-lamination-secrets.md +39 -0
- package/examples/events/assets/content/talks/french-macaron-mastery.md.ejs +39 -0
- package/examples/events/assets/content/talks/neapolitan-pizza-tradition-meets-innovation.md.ejs +39 -0
- package/examples/events/assets/content/talks/savory-breads-of-the-mediterranean.md.ejs +39 -0
- package/examples/events/assets/content/talks/sourdough-from-starter-to-masterpiece.md.ejs +36 -0
- package/examples/events/assets/content/talks/the-art-of-the-perfect-tart.md.ejs +32 -0
- package/examples/events/assets/content/talks/the-science-of-sugar.md.ejs +39 -0
- package/examples/events/assets/content/talks/umami-in-pastry-east-meets-west.md.ejs +39 -0
- package/examples/events/assets/content-collections.ts +56 -0
- package/examples/events/assets/public/background-1.jpg +0 -0
- package/examples/events/assets/public/background-2.jpg +0 -0
- package/examples/events/assets/public/background-3.jpg +0 -0
- package/examples/events/assets/public/background-4.jpg +0 -0
- package/examples/events/assets/public/conference-logo.png +0 -0
- package/examples/events/assets/public/favicon.ico +0 -0
- package/examples/events/assets/public/speakers/andre-costa.jpg +0 -0
- package/examples/events/assets/public/speakers/hans-mueller.jpg +0 -0
- package/examples/events/assets/public/speakers/isabella-martinez.jpg +0 -0
- package/examples/events/assets/public/speakers/kenji-nakamura.jpg +0 -0
- package/examples/events/assets/public/speakers/marie-dubois.jpg +0 -0
- package/examples/events/assets/public/speakers/priya-sharma.jpg +0 -0
- package/examples/events/assets/public/talks/croissant-lamination-secrets.jpg +0 -0
- package/examples/events/assets/public/talks/french-macaron-mastery.jpg +0 -0
- package/examples/events/assets/public/talks/neapolitan-pizza-tradition-meets-innovation.jpg +0 -0
- package/examples/events/assets/public/talks/savory-breads-of-the-mediterranean.jpg +0 -0
- package/examples/events/assets/public/talks/sourdough-from-starter-to-masterpiece.jpg +0 -0
- package/examples/events/assets/public/talks/the-art-of-the-perfect-tart.jpg +0 -0
- package/examples/events/assets/public/talks/the-science-of-sugar.jpg +0 -0
- package/examples/events/assets/public/talks/umami-in-pastry-east-meets-west.jpg +0 -0
- package/examples/events/assets/public/tanstack-circle-logo.png +0 -0
- package/examples/events/assets/public/tanstack-word-logo-white.svg +1 -0
- package/examples/events/assets/src/components/Header.tsx +59 -0
- package/examples/events/assets/src/components/HeaderNav.tsx +67 -0
- package/examples/events/assets/src/components/HeroCarousel.tsx +61 -0
- package/examples/events/assets/src/components/RemyAssistant.tsx +207 -0
- package/examples/events/assets/src/components/SpeakerCard.tsx +67 -0
- package/examples/events/assets/src/components/TalkCard.tsx +77 -0
- package/examples/events/assets/src/components/ui/card.tsx +92 -0
- package/examples/events/assets/src/lib/conference-ai-hook.ts +26 -0
- package/examples/events/assets/src/lib/conference-tools.ts +210 -0
- package/examples/events/assets/src/lib/model-selection.ts +1 -0
- package/examples/events/assets/src/lib/utils.ts +6 -0
- package/examples/events/assets/src/routes/__root.tsx +70 -0
- package/examples/events/assets/src/routes/api.remy-chat.ts +119 -0
- package/examples/events/assets/src/routes/index.tsx +192 -0
- package/examples/events/assets/src/routes/schedule.index.tsx +274 -0
- package/examples/events/assets/src/routes/speakers.$slug.tsx +122 -0
- package/examples/events/assets/src/routes/speakers.index.tsx +40 -0
- package/examples/events/assets/src/routes/talks.$slug.tsx +116 -0
- package/examples/events/assets/src/routes/talks.index.tsx +40 -0
- package/examples/events/assets/src/styles.css +182 -0
- package/examples/events/info.json +74 -0
- package/examples/events/package.json +23 -0
- package/examples/marketing/README.md +60 -0
- package/examples/marketing/assets/public/logo.png +0 -0
- package/examples/marketing/assets/public/motorcycle-adventure.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-cruiser.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-scooter.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-sport.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-supersport.jpg +0 -0
- package/examples/marketing/assets/src/components/Header.tsx +36 -0
- package/examples/marketing/assets/src/components/MotorcycleAIAssistant.tsx +162 -0
- package/examples/marketing/assets/src/components/MotorcycleRecommendation.tsx +53 -0
- package/examples/marketing/assets/src/data/motorcycles.ts.ejs +77 -0
- package/examples/marketing/assets/src/lib/motorcycle-ai-hook.ts +24 -0
- package/examples/marketing/assets/src/lib/motorcycle-tools.ts +42 -0
- package/examples/marketing/assets/src/routes/__root.tsx +57 -0
- package/examples/marketing/assets/src/routes/api.motorcycle-chat.ts +78 -0
- package/examples/marketing/assets/src/routes/index.tsx +72 -0
- package/examples/marketing/assets/src/routes/motorcycles/$motorcycleId.tsx +56 -0
- package/examples/marketing/assets/src/store/motorcycle-assistant.ts +3 -0
- package/examples/marketing/assets/src/styles.css +212 -0
- package/examples/marketing/info.json +38 -0
- package/examples/marketing/package.json +14 -0
- package/examples/resume/README.md +82 -0
- package/examples/resume/assets/content/education/code-school.md +17 -0
- package/examples/resume/assets/content/jobs/freelance.md.ejs +13 -0
- package/examples/resume/assets/content/jobs/initech-junior.md +20 -0
- package/examples/resume/assets/content/jobs/initech-lead.md.ejs +29 -0
- package/examples/resume/assets/content/jobs/initrode-senior.md.ejs +28 -0
- package/examples/resume/assets/content-collections.ts +36 -0
- package/examples/resume/assets/public/headshot-on-white.jpg +0 -0
- package/examples/resume/assets/src/components/Header.tsx +33 -0
- package/examples/resume/assets/src/components/ResumeAssistant.tsx +193 -0
- package/examples/resume/assets/src/components/ui/badge.tsx +46 -0
- package/examples/resume/assets/src/components/ui/card.tsx +92 -0
- package/examples/resume/assets/src/components/ui/checkbox.tsx +30 -0
- package/examples/resume/assets/src/components/ui/hover-card.tsx +44 -0
- package/examples/resume/assets/src/components/ui/separator.tsx +26 -0
- package/examples/resume/assets/src/lib/resume-ai-hook.ts +21 -0
- package/examples/resume/assets/src/lib/resume-tools.ts +165 -0
- package/examples/resume/assets/src/lib/utils.ts +6 -0
- package/examples/resume/assets/src/routes/api.resume-chat.ts +110 -0
- package/examples/resume/assets/src/routes/index.tsx +220 -0
- package/examples/resume/assets/src/styles.css +138 -0
- package/examples/resume/info.json +25 -0
- package/examples/resume/package.json +26 -0
- package/package.json +39 -0
- package/project/base/_dot_claude/skills/content-collections/SKILL.md +505 -0
- package/project/base/_dot_claude/skills/netlify-blobs/SKILL.md +410 -0
- package/project/base/_dot_claude/skills/netlify-db/SKILL.md +424 -0
- package/project/base/_dot_claude/skills/netlify-debugging/SKILL.md +419 -0
- package/project/base/_dot_claude/skills/netlify-forms/SKILL.md +243 -0
- package/project/base/_dot_claude/skills/netlify-functions/SKILL.md +372 -0
- package/project/base/_dot_claude/skills/tanstack-start-api-routes/SKILL.md +421 -0
- package/project/base/_dot_claude/skills/tanstack-start-loaders/SKILL.md +426 -0
- package/project/base/_dot_claude/skills/tanstack-start-project-setup/SKILL.md +493 -0
- package/project/base/_dot_claude/skills/tanstack-start-routes/SKILL.md +430 -0
- package/project/base/_dot_claude/skills/tanstack-start-server-functions/SKILL.md +445 -0
- package/project/base/_dot_claude/skills/tanstack-start-typesafe-routing/SKILL.md +494 -0
- package/project/base/_dot_gitignore +8 -0
- package/project/base/netlify.toml +7 -0
- package/project/base/package.json +33 -0
- package/project/base/public/favicon.ico +0 -0
- package/project/base/public/tanstack-circle-logo.png +0 -0
- package/project/base/public/tanstack-word-logo-white.svg +1 -0
- package/project/base/src/components/Header.tsx +17 -0
- package/project/base/src/components/HeaderNav.tsx.ejs +179 -0
- package/project/base/src/router.tsx +15 -0
- package/project/base/src/routes/__root.tsx +57 -0
- package/project/base/src/routes/index.tsx +48 -0
- package/project/base/src/styles.css +15 -0
- package/project/base/tsconfig.json +28 -0
- package/project/base/vite.config.ts.ejs +25 -0
- package/project/packages.json +22 -0
- package/scripts/check-outdated-packages.js +421 -0
- package/src/cli.ts +343 -0
- package/src/index.ts +49 -0
- package/src/types.ts +15 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-start-loaders
|
|
3
|
+
description: Load data for TanStack Start routes using beforeLoad and loader functions. Use when fetching data for pages, implementing route guards, or setting up route context. IMPORTANT - Loaders should call server functions for data access.
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
metadata:
|
|
6
|
+
author: tanstack
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# TanStack Start Loaders
|
|
11
|
+
|
|
12
|
+
TanStack Start provides `beforeLoad` and `loader` functions for route data loading. Both are **isomorphic** - they run on the server during SSR and on the client during navigation.
|
|
13
|
+
|
|
14
|
+
## Critical Rule
|
|
15
|
+
|
|
16
|
+
**Loaders should call server functions when accessing databases, APIs with secrets, or any server-only resources.** Loaders are isomorphic (run on both server and client), so they cannot directly access server-only code.
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
// ❌ WRONG - Direct database access in loader
|
|
20
|
+
export const Route = createFileRoute('/posts')({
|
|
21
|
+
loader: async () => {
|
|
22
|
+
// This will FAIL on client-side navigation!
|
|
23
|
+
const posts = await db.query('SELECT * FROM posts');
|
|
24
|
+
return { posts };
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ✅ CORRECT - Call server function from loader
|
|
29
|
+
import { getPosts } from '../server/posts.functions';
|
|
30
|
+
|
|
31
|
+
export const Route = createFileRoute('/posts')({
|
|
32
|
+
loader: async () => {
|
|
33
|
+
const posts = await getPosts();
|
|
34
|
+
return { posts };
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## When to Use
|
|
40
|
+
|
|
41
|
+
- **beforeLoad**: Route guards, authentication, setting context
|
|
42
|
+
- **loader**: Fetching data for the route component
|
|
43
|
+
|
|
44
|
+
## beforeLoad vs loader
|
|
45
|
+
|
|
46
|
+
| Feature | beforeLoad | loader |
|
|
47
|
+
|---------|------------|--------|
|
|
48
|
+
| Execution | Sequential (parent → child) | Parallel across routes |
|
|
49
|
+
| Return | Merges into context | Route-specific data |
|
|
50
|
+
| Use case | Guards, auth, context setup | Data fetching |
|
|
51
|
+
|
|
52
|
+
## beforeLoad Function
|
|
53
|
+
|
|
54
|
+
Runs sequentially from parent to child. Use for guards and context:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// src/routes/_protected.tsx
|
|
58
|
+
import { createFileRoute, redirect } from '@tanstack/react-router';
|
|
59
|
+
import { getUser } from '../server/auth.functions';
|
|
60
|
+
|
|
61
|
+
export const Route = createFileRoute('/_protected')({
|
|
62
|
+
beforeLoad: async ({ context }) => {
|
|
63
|
+
// Call server function - NOT direct database access
|
|
64
|
+
const user = await getUser();
|
|
65
|
+
|
|
66
|
+
if (!user) {
|
|
67
|
+
throw redirect({
|
|
68
|
+
to: '/login',
|
|
69
|
+
search: { redirect: location.href },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Return value merges into context for child routes
|
|
74
|
+
return { user };
|
|
75
|
+
},
|
|
76
|
+
component: ProtectedLayout,
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Child routes can access parent's beforeLoad data:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
// src/routes/_protected.dashboard.tsx
|
|
84
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
85
|
+
|
|
86
|
+
export const Route = createFileRoute('/_protected/dashboard')({
|
|
87
|
+
beforeLoad: ({ context }) => {
|
|
88
|
+
// Access user from parent's beforeLoad
|
|
89
|
+
console.log('User:', context.user);
|
|
90
|
+
},
|
|
91
|
+
component: DashboardComponent,
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## loader Function
|
|
96
|
+
|
|
97
|
+
Fetches route-specific data. Runs in parallel across matched routes:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
// src/routes/posts.tsx
|
|
101
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
102
|
+
import { getPosts } from '../server/posts.functions';
|
|
103
|
+
|
|
104
|
+
export const Route = createFileRoute('/posts')({
|
|
105
|
+
loader: async () => {
|
|
106
|
+
// Call server function for data
|
|
107
|
+
const posts = await getPosts();
|
|
108
|
+
return { posts };
|
|
109
|
+
},
|
|
110
|
+
component: PostsComponent,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
function PostsComponent() {
|
|
114
|
+
// Type-safe access to loader data
|
|
115
|
+
const { posts } = Route.useLoaderData();
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<ul>
|
|
119
|
+
{posts.map((post) => (
|
|
120
|
+
<li key={post.id}>{post.title}</li>
|
|
121
|
+
))}
|
|
122
|
+
</ul>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Loader with Parameters
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
// src/routes/posts.$postId.tsx
|
|
131
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
132
|
+
import { getPost } from '../server/posts.functions';
|
|
133
|
+
|
|
134
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
135
|
+
loader: async ({ params }) => {
|
|
136
|
+
const post = await getPost({ data: { id: params.postId } });
|
|
137
|
+
|
|
138
|
+
if (!post) {
|
|
139
|
+
throw new Error('Post not found');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { post };
|
|
143
|
+
},
|
|
144
|
+
component: PostComponent,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
function PostComponent() {
|
|
148
|
+
const { post } = Route.useLoaderData();
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<article>
|
|
152
|
+
<h1>{post.title}</h1>
|
|
153
|
+
<p>{post.content}</p>
|
|
154
|
+
</article>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Loader Dependencies (loaderDeps)
|
|
160
|
+
|
|
161
|
+
Re-run loader when search params change:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
// src/routes/posts.tsx
|
|
165
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
166
|
+
import { searchPosts } from '../server/posts.functions';
|
|
167
|
+
|
|
168
|
+
export const Route = createFileRoute('/posts')({
|
|
169
|
+
validateSearch: (search) => ({
|
|
170
|
+
page: Number(search.page) || 1,
|
|
171
|
+
filter: search.filter as string | undefined,
|
|
172
|
+
}),
|
|
173
|
+
|
|
174
|
+
// Define which values trigger loader re-runs
|
|
175
|
+
loaderDeps: ({ search }) => ({
|
|
176
|
+
page: search.page,
|
|
177
|
+
filter: search.filter,
|
|
178
|
+
}),
|
|
179
|
+
|
|
180
|
+
loader: async ({ deps }) => {
|
|
181
|
+
// deps contains values from loaderDeps
|
|
182
|
+
const { posts, total } = await searchPosts({
|
|
183
|
+
data: { page: deps.page, filter: deps.filter }
|
|
184
|
+
});
|
|
185
|
+
return { posts, total, page: deps.page };
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
component: PostsComponent,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
function PostsComponent() {
|
|
192
|
+
const { posts, total, page } = Route.useLoaderData();
|
|
193
|
+
const navigate = useNavigate();
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div>
|
|
197
|
+
<ul>
|
|
198
|
+
{posts.map((post) => (
|
|
199
|
+
<li key={post.id}>{post.title}</li>
|
|
200
|
+
))}
|
|
201
|
+
</ul>
|
|
202
|
+
|
|
203
|
+
<button
|
|
204
|
+
onClick={() => navigate({ search: { page: page + 1 } })}
|
|
205
|
+
>
|
|
206
|
+
Next Page
|
|
207
|
+
</button>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Deferred Data Loading
|
|
214
|
+
|
|
215
|
+
Load critical data first, stream non-critical data:
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
// src/routes/posts.$postId.tsx
|
|
219
|
+
import { createFileRoute, Await } from '@tanstack/react-router';
|
|
220
|
+
import { getPost, getComments, getRelatedPosts } from '../server/posts.functions';
|
|
221
|
+
|
|
222
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
223
|
+
loader: async ({ params }) => {
|
|
224
|
+
// Critical data - await it
|
|
225
|
+
const post = await getPost({ data: { id: params.postId } });
|
|
226
|
+
|
|
227
|
+
// Non-critical data - don't await, stream later
|
|
228
|
+
const commentsPromise = getComments({ data: { postId: params.postId } });
|
|
229
|
+
const relatedPromise = getRelatedPosts({ data: { postId: params.postId } });
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
post,
|
|
233
|
+
comments: commentsPromise, // Promise, not resolved
|
|
234
|
+
related: relatedPromise, // Promise, not resolved
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
component: PostComponent,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
function PostComponent() {
|
|
241
|
+
const { post, comments, related } = Route.useLoaderData();
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<article>
|
|
245
|
+
<h1>{post.title}</h1>
|
|
246
|
+
<p>{post.content}</p>
|
|
247
|
+
|
|
248
|
+
{/* Comments stream in when ready */}
|
|
249
|
+
<Suspense fallback={<p>Loading comments...</p>}>
|
|
250
|
+
<Await promise={comments}>
|
|
251
|
+
{(resolvedComments) => (
|
|
252
|
+
<section>
|
|
253
|
+
<h2>Comments</h2>
|
|
254
|
+
{resolvedComments.map((c) => (
|
|
255
|
+
<div key={c.id}>{c.text}</div>
|
|
256
|
+
))}
|
|
257
|
+
</section>
|
|
258
|
+
)}
|
|
259
|
+
</Await>
|
|
260
|
+
</Suspense>
|
|
261
|
+
|
|
262
|
+
{/* Related posts stream in when ready */}
|
|
263
|
+
<Suspense fallback={<p>Loading related...</p>}>
|
|
264
|
+
<Await promise={related}>
|
|
265
|
+
{(resolvedRelated) => (
|
|
266
|
+
<aside>
|
|
267
|
+
<h2>Related Posts</h2>
|
|
268
|
+
<ul>
|
|
269
|
+
{resolvedRelated.map((p) => (
|
|
270
|
+
<li key={p.id}>{p.title}</li>
|
|
271
|
+
))}
|
|
272
|
+
</ul>
|
|
273
|
+
</aside>
|
|
274
|
+
)}
|
|
275
|
+
</Await>
|
|
276
|
+
</Suspense>
|
|
277
|
+
</article>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Context from Router
|
|
283
|
+
|
|
284
|
+
Pass initial context from router creation:
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
// src/router.tsx
|
|
288
|
+
import { createRouter } from '@tanstack/react-router';
|
|
289
|
+
import { routeTree } from './routeTree.gen';
|
|
290
|
+
|
|
291
|
+
export function getRouter() {
|
|
292
|
+
return createRouter({
|
|
293
|
+
routeTree,
|
|
294
|
+
context: {
|
|
295
|
+
// Initial context available to all routes
|
|
296
|
+
queryClient: new QueryClient(),
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Access in loaders:
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
export const Route = createFileRoute('/posts')({
|
|
306
|
+
loader: async ({ context }) => {
|
|
307
|
+
// Access router context
|
|
308
|
+
return context.queryClient.ensureQueryData(postsQueryOptions());
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Error Handling
|
|
314
|
+
|
|
315
|
+
```tsx
|
|
316
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
317
|
+
loader: async ({ params }) => {
|
|
318
|
+
const post = await getPost({ data: { id: params.postId } });
|
|
319
|
+
|
|
320
|
+
if (!post) {
|
|
321
|
+
throw new Error('Post not found');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return { post };
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
// Custom error component
|
|
328
|
+
errorComponent: ({ error }) => (
|
|
329
|
+
<div>
|
|
330
|
+
<h1>Error Loading Post</h1>
|
|
331
|
+
<p>{error.message}</p>
|
|
332
|
+
<Link to="/posts">Back to Posts</Link>
|
|
333
|
+
</div>
|
|
334
|
+
),
|
|
335
|
+
|
|
336
|
+
component: PostComponent,
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Pending/Loading States
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
export const Route = createFileRoute('/posts')({
|
|
344
|
+
loader: async () => {
|
|
345
|
+
const posts = await getPosts();
|
|
346
|
+
return { posts };
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
// Show while loading
|
|
350
|
+
pendingComponent: () => <div>Loading posts...</div>,
|
|
351
|
+
|
|
352
|
+
// Minimum time to show pending (prevents flash)
|
|
353
|
+
pendingMinMs: 200,
|
|
354
|
+
|
|
355
|
+
// Delay before showing pending
|
|
356
|
+
pendingMs: 100,
|
|
357
|
+
|
|
358
|
+
component: PostsComponent,
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Stale Time Configuration
|
|
363
|
+
|
|
364
|
+
Control when loaders re-run:
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
export const Route = createFileRoute('/posts')({
|
|
368
|
+
// Data is fresh for 5 minutes
|
|
369
|
+
staleTime: 5 * 60 * 1000,
|
|
370
|
+
|
|
371
|
+
// Preload data is fresh for 30 seconds
|
|
372
|
+
preloadStaleTime: 30 * 1000,
|
|
373
|
+
|
|
374
|
+
// Garbage collect after 10 minutes
|
|
375
|
+
gcTime: 10 * 60 * 1000,
|
|
376
|
+
|
|
377
|
+
loader: async () => {
|
|
378
|
+
return { posts: await getPosts() };
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Using with TanStack Query
|
|
384
|
+
|
|
385
|
+
For complex caching needs, use TanStack Query with loaders:
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
389
|
+
import { useSuspenseQuery, queryOptions } from '@tanstack/react-query';
|
|
390
|
+
import { getPosts } from '../server/posts.functions';
|
|
391
|
+
|
|
392
|
+
const postsQueryOptions = () => queryOptions({
|
|
393
|
+
queryKey: ['posts'],
|
|
394
|
+
queryFn: () => getPosts(),
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
export const Route = createFileRoute('/posts')({
|
|
398
|
+
loader: async ({ context }) => {
|
|
399
|
+
// Ensure data is in cache before rendering
|
|
400
|
+
await context.queryClient.ensureQueryData(postsQueryOptions());
|
|
401
|
+
},
|
|
402
|
+
component: PostsComponent,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
function PostsComponent() {
|
|
406
|
+
// Use query hook for reactive updates
|
|
407
|
+
const { data: posts } = useSuspenseQuery(postsQueryOptions());
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<ul>
|
|
411
|
+
{posts.map((post) => (
|
|
412
|
+
<li key={post.id}>{post.title}</li>
|
|
413
|
+
))}
|
|
414
|
+
</ul>
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Summary
|
|
420
|
+
|
|
421
|
+
1. **Always use server functions** in loaders for server-only operations
|
|
422
|
+
2. **beforeLoad** for guards and context (sequential)
|
|
423
|
+
3. **loader** for data fetching (parallel)
|
|
424
|
+
4. Use **loaderDeps** to re-run on search param changes
|
|
425
|
+
5. **Defer** non-critical data with promises
|
|
426
|
+
6. Configure **staleTime** for caching behavior
|