@prmichaelsen/acp-visualizer 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/README.md +68 -0
- package/agent/commands/acp.clarification-address.md +417 -0
- package/agent/commands/acp.clarification-capture.md +386 -0
- package/agent/commands/acp.clarification-create.md +437 -0
- package/agent/commands/acp.clarifications-research.md +326 -0
- package/agent/commands/acp.command-create.md +432 -0
- package/agent/commands/acp.design-create.md +286 -0
- package/agent/commands/acp.design-reference.md +355 -0
- package/agent/commands/acp.handoff.md +270 -0
- package/agent/commands/acp.index.md +423 -0
- package/agent/commands/acp.init.md +546 -0
- package/agent/commands/acp.package-create.md +895 -0
- package/agent/commands/acp.package-info.md +212 -0
- package/agent/commands/acp.package-install.md +539 -0
- package/agent/commands/acp.package-list.md +280 -0
- package/agent/commands/acp.package-publish.md +541 -0
- package/agent/commands/acp.package-remove.md +293 -0
- package/agent/commands/acp.package-search.md +307 -0
- package/agent/commands/acp.package-update.md +361 -0
- package/agent/commands/acp.package-validate.md +540 -0
- package/agent/commands/acp.pattern-create.md +386 -0
- package/agent/commands/acp.plan.md +587 -0
- package/agent/commands/acp.proceed.md +882 -0
- package/agent/commands/acp.project-create.md +675 -0
- package/agent/commands/acp.project-info.md +312 -0
- package/agent/commands/acp.project-list.md +226 -0
- package/agent/commands/acp.project-remove.md +379 -0
- package/agent/commands/acp.project-set.md +227 -0
- package/agent/commands/acp.project-update.md +307 -0
- package/agent/commands/acp.projects-restore.md +228 -0
- package/agent/commands/acp.projects-sync.md +347 -0
- package/agent/commands/acp.report.md +407 -0
- package/agent/commands/acp.resume.md +239 -0
- package/agent/commands/acp.sessions.md +301 -0
- package/agent/commands/acp.status.md +293 -0
- package/agent/commands/acp.sync.md +364 -0
- package/agent/commands/acp.task-create.md +500 -0
- package/agent/commands/acp.update.md +302 -0
- package/agent/commands/acp.validate.md +466 -0
- package/agent/commands/acp.version-check-for-updates.md +276 -0
- package/agent/commands/acp.version-check.md +191 -0
- package/agent/commands/acp.version-update.md +289 -0
- package/agent/commands/command.template.md +339 -0
- package/agent/commands/git.commit.md +526 -0
- package/agent/commands/git.init.md +514 -0
- package/agent/commands/tanstack-cloudflare.deploy.md +272 -0
- package/agent/commands/tanstack-cloudflare.tail.md +275 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/design.template.md +154 -0
- package/agent/design/local.dashboard-layout-routing.md +288 -0
- package/agent/design/local.data-model-yaml-parsing.md +310 -0
- package/agent/design/local.search-filtering.md +331 -0
- package/agent/design/local.server-api-auto-refresh.md +235 -0
- package/agent/design/local.table-tree-views.md +299 -0
- package/agent/design/local.visualizer-requirements.md +349 -0
- package/agent/design/requirements.template.md +387 -0
- package/agent/index/.gitkeep +0 -0
- package/agent/index/acp.core.yaml +137 -0
- package/agent/index/local.main.template.yaml +37 -0
- package/agent/manifest.template.yaml +13 -0
- package/agent/manifest.yaml +302 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +67 -0
- package/agent/milestones/milestone-1-{title}.template.md +206 -0
- package/agent/milestones/milestone-2-dashboard-views-interaction.md +79 -0
- package/agent/package.template.yaml +86 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.template.md +1237 -0
- package/agent/patterns/pattern.template.md +382 -0
- package/agent/patterns/tanstack-cloudflare.acl-permissions.md +332 -0
- package/agent/patterns/tanstack-cloudflare.action-bar-item.md +416 -0
- package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +401 -0
- package/agent/patterns/tanstack-cloudflare.auth-session-management.md +387 -0
- package/agent/patterns/tanstack-cloudflare.card-and-list.md +271 -0
- package/agent/patterns/tanstack-cloudflare.chat-engine.md +353 -0
- package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +346 -0
- package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +516 -0
- package/agent/patterns/tanstack-cloudflare.email-service.md +431 -0
- package/agent/patterns/tanstack-cloudflare.expander.md +98 -0
- package/agent/patterns/tanstack-cloudflare.fcm-push.md +115 -0
- package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +441 -0
- package/agent/patterns/tanstack-cloudflare.firebase-auth.md +348 -0
- package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +550 -0
- package/agent/patterns/tanstack-cloudflare.firebase-storage.md +369 -0
- package/agent/patterns/tanstack-cloudflare.form-controls.md +145 -0
- package/agent/patterns/tanstack-cloudflare.global-search-context.md +93 -0
- package/agent/patterns/tanstack-cloudflare.image-carousel.md +126 -0
- package/agent/patterns/tanstack-cloudflare.library-services.md +553 -0
- package/agent/patterns/tanstack-cloudflare.lightbox.md +169 -0
- package/agent/patterns/tanstack-cloudflare.markdown-content.md +115 -0
- package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +98 -0
- package/agent/patterns/tanstack-cloudflare.modal.md +156 -0
- package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +461 -0
- package/agent/patterns/tanstack-cloudflare.notifications-engine.md +151 -0
- package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +90 -0
- package/agent/patterns/tanstack-cloudflare.og-metadata.md +296 -0
- package/agent/patterns/tanstack-cloudflare.pagination.md +442 -0
- package/agent/patterns/tanstack-cloudflare.pill-input.md +220 -0
- package/agent/patterns/tanstack-cloudflare.provider-adapter.md +401 -0
- package/agent/patterns/tanstack-cloudflare.rate-limiting.md +323 -0
- package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +338 -0
- package/agent/patterns/tanstack-cloudflare.searchable-settings.md +375 -0
- package/agent/patterns/tanstack-cloudflare.slide-over.md +129 -0
- package/agent/patterns/tanstack-cloudflare.ssr-preload.md +571 -0
- package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +508 -0
- package/agent/patterns/tanstack-cloudflare.toast-system.md +142 -0
- package/agent/patterns/tanstack-cloudflare.unified-header.md +280 -0
- package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +628 -0
- package/agent/patterns/tanstack-cloudflare.websocket-manager.md +237 -0
- package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +358 -0
- package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +336 -0
- package/agent/progress.template.yaml +161 -0
- package/agent/progress.yaml +145 -0
- package/agent/schemas/package.schema.yaml +276 -0
- package/agent/scripts/acp.common.sh +1781 -0
- package/agent/scripts/acp.install.sh +333 -0
- package/agent/scripts/acp.package-create.sh +924 -0
- package/agent/scripts/acp.package-info.sh +288 -0
- package/agent/scripts/acp.package-install.sh +893 -0
- package/agent/scripts/acp.package-list.sh +311 -0
- package/agent/scripts/acp.package-publish.sh +420 -0
- package/agent/scripts/acp.package-remove.sh +348 -0
- package/agent/scripts/acp.package-search.sh +156 -0
- package/agent/scripts/acp.package-update.sh +517 -0
- package/agent/scripts/acp.package-validate.sh +1018 -0
- package/agent/scripts/acp.uninstall.sh +85 -0
- package/agent/scripts/acp.version-check-for-updates.sh +98 -0
- package/agent/scripts/acp.version-check.sh +47 -0
- package/agent/scripts/acp.version-update.sh +176 -0
- package/agent/scripts/acp.yaml-parser.sh +985 -0
- package/agent/scripts/acp.yaml-validate.sh +205 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +210 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +294 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +193 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +262 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +156 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +178 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +141 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +153 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +174 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +233 -0
- package/agent/tasks/task-1-{title}.template.md +244 -0
- package/bin/visualize.mjs +84 -0
- package/package.json +48 -0
- package/src/components/ExtraFieldsBadge.tsx +15 -0
- package/src/components/FilterBar.tsx +33 -0
- package/src/components/Header.tsx +23 -0
- package/src/components/MilestoneTable.tsx +167 -0
- package/src/components/MilestoneTree.tsx +84 -0
- package/src/components/ProgressBar.tsx +20 -0
- package/src/components/SearchInput.tsx +22 -0
- package/src/components/Sidebar.tsx +54 -0
- package/src/components/StatusBadge.tsx +23 -0
- package/src/components/StatusDot.tsx +12 -0
- package/src/components/TaskList.tsx +36 -0
- package/src/components/ViewToggle.tsx +31 -0
- package/src/lib/config.ts +8 -0
- package/src/lib/file-watcher.ts +43 -0
- package/src/lib/search.ts +48 -0
- package/src/lib/types.ts +73 -0
- package/src/lib/useAutoRefresh.ts +31 -0
- package/src/lib/useCollapse.ts +31 -0
- package/src/lib/useFilteredData.ts +55 -0
- package/src/lib/yaml-loader-real.spec.ts +47 -0
- package/src/lib/yaml-loader.spec.ts +201 -0
- package/src/lib/yaml-loader.ts +265 -0
- package/src/routeTree.gen.ts +140 -0
- package/src/router.tsx +10 -0
- package/src/routes/__root.tsx +75 -0
- package/src/routes/api/watch.ts +29 -0
- package/src/routes/index.tsx +115 -0
- package/src/routes/milestones.tsx +50 -0
- package/src/routes/search.tsx +84 -0
- package/src/routes/tasks.tsx +63 -0
- package/src/services/progress-database.service.ts +46 -0
- package/src/styles.css +25 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +16 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
# SSR Data Preloading Pattern
|
|
2
|
+
|
|
3
|
+
**Category**: Architecture
|
|
4
|
+
**Applicable To**: TanStack Start + Cloudflare Workers applications requiring server-side rendering
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
This pattern demonstrates how to preload data on the server using TanStack Router's `beforeLoad` hook, pass it through route context, and hydrate components with SSR data to eliminate loading flashes. By fetching data server-side before the page renders, you provide instant content to users and search engines while maintaining the ability to add real-time updates via WebSockets.
|
|
12
|
+
|
|
13
|
+
The pattern ensures that users never see loading spinners on initial page load, improving perceived performance and providing better SEO since search engines can index the fully-rendered content.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## When to Use This Pattern
|
|
18
|
+
|
|
19
|
+
✅ **Use this pattern when:**
|
|
20
|
+
- Building TanStack Start applications with Cloudflare Workers
|
|
21
|
+
- Any data can be fetched server-side (Firestore, database, API)
|
|
22
|
+
- Working with user-specific data (we use server-side auth, never client-side)
|
|
23
|
+
- Need to eliminate loading spinners on initial page load
|
|
24
|
+
- Want better SEO (search engines can index the content)
|
|
25
|
+
- Improving perceived performance is important
|
|
26
|
+
- Working with real-time data (SSR provides initial state, WebSocket adds updates)
|
|
27
|
+
|
|
28
|
+
❌ **Don't use this pattern when:**
|
|
29
|
+
- Data is too large (>500KB) and would slow down SSR
|
|
30
|
+
- Data fetch is extremely slow (>3 seconds) and would block page load
|
|
31
|
+
- Data is purely client-side (localStorage, IndexedDB)
|
|
32
|
+
- Building static pages with no dynamic data
|
|
33
|
+
|
|
34
|
+
**Note**: We ALWAYS use server-side auth (`getAuthSession`), never client-side. Real-time listeners are attached AFTER SSR hydration.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Core Principles
|
|
39
|
+
|
|
40
|
+
1. **Server-First Data Loading**: Fetch data on the server before rendering, not after
|
|
41
|
+
2. **beforeLoad Over loader**: Use `beforeLoad` (not `loader`) for SSR data preloading in TanStack Start
|
|
42
|
+
3. **Route Context for Data**: Pass preloaded data through route context, not loader data
|
|
43
|
+
4. **Graceful Degradation**: Handle errors gracefully - don't fail page load if data fetch fails
|
|
44
|
+
5. **Skip Client Fetch**: Components check for SSR data and skip client-side fetch if present
|
|
45
|
+
6. **WebSocket After Hydration**: Real-time listeners attach after SSR hydration completes
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Implementation
|
|
50
|
+
|
|
51
|
+
### Structure
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
src/
|
|
55
|
+
├── routes/
|
|
56
|
+
│ └── your-route.tsx # Route with beforeLoad
|
|
57
|
+
├── components/
|
|
58
|
+
│ └── YourComponent.tsx # Component using initialData
|
|
59
|
+
└── services/
|
|
60
|
+
└── your-database.service.ts # Database service for SSR
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Code Example
|
|
64
|
+
|
|
65
|
+
#### Step 1: Route Configuration with beforeLoad
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// src/routes/your-route.tsx
|
|
69
|
+
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
70
|
+
import { getAuthSession } from '@/lib/auth/server-fn'
|
|
71
|
+
import { YourDatabaseService } from '@/services/your-database.service'
|
|
72
|
+
import type { YourDataType } from '@/types/your-types'
|
|
73
|
+
|
|
74
|
+
export const Route = createFileRoute('/your-route')({
|
|
75
|
+
beforeLoad: async () => {
|
|
76
|
+
// 1. Check authentication (if needed)
|
|
77
|
+
const user = await getAuthSession()
|
|
78
|
+
|
|
79
|
+
if (!user) {
|
|
80
|
+
throw redirect({
|
|
81
|
+
to: '/auth',
|
|
82
|
+
search: { redirect_url: '/your-route' },
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 2. Preload data with proper typing
|
|
87
|
+
let initialData: YourDataType[] = []
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
initialData = await YourDatabaseService.getData(user.uid, 50)
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('Failed to preload data:', error)
|
|
93
|
+
// Continue with empty data - not fatal
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 3. Return data through context
|
|
97
|
+
return {
|
|
98
|
+
user,
|
|
99
|
+
initialData
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
component: YourComponent,
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Step 2: Component Data Access
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// src/routes/your-route.tsx (continued)
|
|
110
|
+
function YourComponent() {
|
|
111
|
+
// Get data from route context (NOT useLoaderData)
|
|
112
|
+
const context = Route.useRouteContext()
|
|
113
|
+
const { user, initialData } = context
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div>
|
|
117
|
+
<YourChildComponent initialData={initialData} />
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Step 3: Child Component Integration
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// src/components/YourChildComponent.tsx
|
|
127
|
+
interface YourChildComponentProps {
|
|
128
|
+
initialData?: YourDataType[] // SSR data
|
|
129
|
+
className?: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function YourChildComponent({
|
|
133
|
+
initialData = [], // Default to empty array
|
|
134
|
+
className
|
|
135
|
+
}: YourChildComponentProps) {
|
|
136
|
+
const [data, setData] = useState<YourDataType[]>(initialData) // Initialize with SSR data
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
// Skip loading if we have SSR data
|
|
140
|
+
if (initialData.length > 0) {
|
|
141
|
+
console.log('Using SSR data, skipping client fetch')
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Only load if no SSR data
|
|
146
|
+
loadData()
|
|
147
|
+
}, [initialData.length])
|
|
148
|
+
|
|
149
|
+
// Component renders immediately with SSR data!
|
|
150
|
+
return (
|
|
151
|
+
<div className={className}>
|
|
152
|
+
{data.map(item => (
|
|
153
|
+
<div key={item.id}>{item.name}</div>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Examples
|
|
163
|
+
|
|
164
|
+
### Example 1: Chat Messages with SSR
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// src/routes/chat.tsx
|
|
168
|
+
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
169
|
+
import { getAuthSession } from '@/lib/auth/server-fn'
|
|
170
|
+
import { ConversationDatabaseService } from '@/services/conversation-database.service'
|
|
171
|
+
import type { Message } from '@/types/chat'
|
|
172
|
+
|
|
173
|
+
export const Route = createFileRoute('/chat')({
|
|
174
|
+
beforeLoad: async () => {
|
|
175
|
+
const user = await getAuthSession()
|
|
176
|
+
if (!user) throw redirect({ to: '/auth' })
|
|
177
|
+
|
|
178
|
+
const conversationId = 'main'
|
|
179
|
+
let initialMessages: Message[] = []
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
initialMessages = await ConversationDatabaseService.getMessages(
|
|
183
|
+
user.uid,
|
|
184
|
+
conversationId,
|
|
185
|
+
50
|
|
186
|
+
)
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error('Failed to preload messages:', error)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { user, conversationId, initialMessages }
|
|
192
|
+
},
|
|
193
|
+
component: Chat,
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
function Chat() {
|
|
197
|
+
const { user, conversationId, initialMessages } = Route.useRouteContext()
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<ChatInterface
|
|
201
|
+
conversationId={conversationId}
|
|
202
|
+
initialMessages={initialMessages}
|
|
203
|
+
/>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Example 2: WebSocket Integration with SSR
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// src/components/chat/ChatInterface.tsx
|
|
212
|
+
interface ChatInterfaceProps {
|
|
213
|
+
conversationId?: string
|
|
214
|
+
initialMessages?: Message[]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function ChatInterface({
|
|
218
|
+
conversationId,
|
|
219
|
+
initialMessages = [],
|
|
220
|
+
}: ChatInterfaceProps) {
|
|
221
|
+
const [messages, setMessages] = useState<Message[]>(initialMessages)
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
const wsClient = new ChatWebSocket({
|
|
225
|
+
onConnectionChange: (isConnected) => {
|
|
226
|
+
// Skip loading if we have SSR data
|
|
227
|
+
if (initialMessages.length > 0) {
|
|
228
|
+
console.log('Skipping WebSocket load - using SSR data')
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Load data via WebSocket if no SSR data
|
|
233
|
+
if (isConnected) {
|
|
234
|
+
wsClient.loadMessages(conversationId)
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
onMessageReceived: (newMessage) => {
|
|
238
|
+
// Add new messages from WebSocket
|
|
239
|
+
setMessages(prev => [...prev, newMessage])
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
return () => wsClient.disconnect()
|
|
244
|
+
}, [initialMessages.length])
|
|
245
|
+
|
|
246
|
+
// Component renders immediately with SSR data!
|
|
247
|
+
return (
|
|
248
|
+
<div>
|
|
249
|
+
{messages.map(msg => (
|
|
250
|
+
<div key={msg.id}>{msg.content}</div>
|
|
251
|
+
))}
|
|
252
|
+
</div>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Example 3: User Profile with SSR
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// src/routes/profile.tsx
|
|
261
|
+
export const Route = createFileRoute('/profile')({
|
|
262
|
+
beforeLoad: async () => {
|
|
263
|
+
const user = await getAuthSession()
|
|
264
|
+
if (!user) throw redirect({ to: '/auth' })
|
|
265
|
+
|
|
266
|
+
let profile = null
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
profile = await UserDatabaseService.getProfile(user.uid)
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error('Failed to load profile:', error)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { user, profile }
|
|
275
|
+
},
|
|
276
|
+
component: Profile,
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
function Profile() {
|
|
280
|
+
const { user, profile } = Route.useRouteContext()
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<ProfileView initialProfile={profile} />
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Benefits
|
|
291
|
+
|
|
292
|
+
### 1. Instant Content Display
|
|
293
|
+
|
|
294
|
+
Users see content immediately without loading spinners. The page is fully rendered on the server with real data.
|
|
295
|
+
|
|
296
|
+
**Before SSR**: Page loads → Show spinner → Fetch data → Render data (2-3 seconds)
|
|
297
|
+
**After SSR**: Page loads → Data already rendered (<1 second)
|
|
298
|
+
|
|
299
|
+
### 2. Better SEO
|
|
300
|
+
|
|
301
|
+
Search engines can index the fully-rendered content since data is present in the initial HTML.
|
|
302
|
+
|
|
303
|
+
### 3. Improved Perceived Performance
|
|
304
|
+
|
|
305
|
+
Users perceive the application as faster because they see content immediately, even if real-time updates take a moment to connect.
|
|
306
|
+
|
|
307
|
+
### 4. Reduced Client-Side Complexity
|
|
308
|
+
|
|
309
|
+
Components don't need complex loading states for initial data - they start with data already present.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Trade-offs
|
|
314
|
+
|
|
315
|
+
### 1. Server-Side Execution Time
|
|
316
|
+
|
|
317
|
+
**Downside**: Data fetching happens on the server, which can slow down initial page load if queries are slow.
|
|
318
|
+
|
|
319
|
+
**Mitigation**:
|
|
320
|
+
- Set timeouts on data fetches (fail gracefully)
|
|
321
|
+
- Only preload essential data
|
|
322
|
+
- Use caching where appropriate
|
|
323
|
+
- Consider skipping SSR for very slow queries
|
|
324
|
+
|
|
325
|
+
### 2. Increased Server Load
|
|
326
|
+
|
|
327
|
+
**Downside**: Every page request executes server-side data fetching, increasing server resource usage.
|
|
328
|
+
|
|
329
|
+
**Mitigation**:
|
|
330
|
+
- Use Cloudflare Workers' edge caching
|
|
331
|
+
- Implement query result caching
|
|
332
|
+
- Monitor server performance
|
|
333
|
+
- Scale horizontally if needed
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Anti-Patterns
|
|
338
|
+
|
|
339
|
+
### ❌ Anti-Pattern 1: Using loader Instead of beforeLoad
|
|
340
|
+
|
|
341
|
+
**Description**: Using TanStack Router's `loader` instead of `beforeLoad` for SSR data preloading.
|
|
342
|
+
|
|
343
|
+
**Why it's bad**: May not work correctly in TanStack Start setup, inconsistent with project patterns.
|
|
344
|
+
|
|
345
|
+
**Instead, do this**: Use `beforeLoad` for SSR data preloading.
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// ❌ Bad: Using loader
|
|
349
|
+
export const Route = createFileRoute('/route')({
|
|
350
|
+
loader: async () => {
|
|
351
|
+
return { data: await fetchData() }
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
// ✅ Good: Using beforeLoad
|
|
356
|
+
export const Route = createFileRoute('/route')({
|
|
357
|
+
beforeLoad: async () => {
|
|
358
|
+
return { data: await fetchData() }
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### ❌ Anti-Pattern 2: Forgetting Type Annotation
|
|
364
|
+
|
|
365
|
+
**Description**: Not providing explicit type annotation for initialData variable.
|
|
366
|
+
|
|
367
|
+
**Why it's bad**: TypeScript can't infer the type, leading to `any[]` and loss of type safety.
|
|
368
|
+
|
|
369
|
+
**Instead, do this**: Always provide explicit type annotation.
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
// ❌ Bad: Implicit any[]
|
|
373
|
+
let initialData = []
|
|
374
|
+
|
|
375
|
+
// ✅ Good: Explicit type
|
|
376
|
+
let initialData: Message[] = []
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### ❌ Anti-Pattern 3: Always Fetching on Client
|
|
380
|
+
|
|
381
|
+
**Description**: Component always fetches data on mount, ignoring SSR data.
|
|
382
|
+
|
|
383
|
+
**Why it's bad**: Wastes the SSR data, causes unnecessary re-renders, shows loading state unnecessarily.
|
|
384
|
+
|
|
385
|
+
**Instead, do this**: Check for SSR data first.
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// ❌ Bad: Always fetching
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
loadData() // Always runs, ignores SSR data
|
|
391
|
+
}, [])
|
|
392
|
+
|
|
393
|
+
// ✅ Good: Check for SSR data
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
if (initialData.length > 0) return // Skip if SSR data exists
|
|
396
|
+
loadData()
|
|
397
|
+
}, [initialData.length])
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### ❌ Anti-Pattern 4: Failing Page Load on Data Error
|
|
401
|
+
|
|
402
|
+
**Description**: Throwing errors in `beforeLoad` when data fetch fails.
|
|
403
|
+
|
|
404
|
+
**Why it's bad**: Prevents page from loading at all, poor user experience.
|
|
405
|
+
|
|
406
|
+
**Instead, do this**: Handle errors gracefully and continue with empty data.
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
// ❌ Bad: Throwing on error
|
|
410
|
+
beforeLoad: async () => {
|
|
411
|
+
const data = await fetchData() // Throws if fails
|
|
412
|
+
return { data }
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ✅ Good: Graceful error handling
|
|
416
|
+
beforeLoad: async () => {
|
|
417
|
+
let data = []
|
|
418
|
+
try {
|
|
419
|
+
data = await fetchData()
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.error('Failed to preload:', error)
|
|
422
|
+
// Continue with empty data
|
|
423
|
+
}
|
|
424
|
+
return { data }
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Testing Strategy
|
|
431
|
+
|
|
432
|
+
### Unit Testing Components with SSR Data
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
import { render, screen } from '@testing-library/react'
|
|
436
|
+
import { YourComponent } from './YourComponent'
|
|
437
|
+
|
|
438
|
+
describe('YourComponent with SSR', () => {
|
|
439
|
+
it('should render with SSR data', () => {
|
|
440
|
+
const initialData = [
|
|
441
|
+
{ id: '1', name: 'Item 1' },
|
|
442
|
+
{ id: '2', name: 'Item 2' },
|
|
443
|
+
]
|
|
444
|
+
|
|
445
|
+
render(<YourComponent initialData={initialData} />)
|
|
446
|
+
|
|
447
|
+
expect(screen.getByText('Item 1')).toBeInTheDocument()
|
|
448
|
+
expect(screen.getByText('Item 2')).toBeInTheDocument()
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('should not fetch data when SSR data provided', () => {
|
|
452
|
+
const loadDataSpy = jest.fn()
|
|
453
|
+
const initialData = [{ id: '1', name: 'Item 1' }]
|
|
454
|
+
|
|
455
|
+
render(<YourComponent initialData={initialData} />)
|
|
456
|
+
|
|
457
|
+
expect(loadDataSpy).not.toHaveBeenCalled()
|
|
458
|
+
})
|
|
459
|
+
})
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Testing SSR with JavaScript Disabled
|
|
463
|
+
|
|
464
|
+
1. Disable JavaScript in browser
|
|
465
|
+
2. Navigate to route
|
|
466
|
+
3. Data should still render (proves SSR works)
|
|
467
|
+
|
|
468
|
+
### Testing Hydration
|
|
469
|
+
|
|
470
|
+
1. Enable JavaScript
|
|
471
|
+
2. Check console for "Using SSR data" log
|
|
472
|
+
3. Verify no loading spinner appears
|
|
473
|
+
4. Verify no duplicate data fetches
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Related Patterns
|
|
478
|
+
|
|
479
|
+
- **[Library Services Pattern](./tanstack-cloudflare.library-services.md)**: Database services are used in `beforeLoad` for server-side data fetching
|
|
480
|
+
- **[User-Scoped Collections](./tanstack-cloudflare.user-scoped-collections.md)**: SSR preloading works with user-scoped Firestore collections
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Migration Guide
|
|
485
|
+
|
|
486
|
+
### Step 1: Identify Client-Side Data Fetching
|
|
487
|
+
|
|
488
|
+
Find components that fetch data in `useEffect`:
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
// Current pattern
|
|
492
|
+
useEffect(() => {
|
|
493
|
+
fetchData().then(setData)
|
|
494
|
+
}, [])
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Step 2: Add beforeLoad to Route
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
// Add to route file
|
|
501
|
+
export const Route = createFileRoute('/route')({
|
|
502
|
+
beforeLoad: async () => {
|
|
503
|
+
let initialData = []
|
|
504
|
+
try {
|
|
505
|
+
initialData = await YourDatabaseService.getData()
|
|
506
|
+
} catch (error) {
|
|
507
|
+
console.error('Preload failed:', error)
|
|
508
|
+
}
|
|
509
|
+
return { initialData }
|
|
510
|
+
},
|
|
511
|
+
component: YourComponent,
|
|
512
|
+
})
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Step 3: Update Component to Accept initialData
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
// Update component props
|
|
519
|
+
interface Props {
|
|
520
|
+
initialData?: DataType[]
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function YourComponent({ initialData = [] }: Props) {
|
|
524
|
+
const [data, setData] = useState(initialData)
|
|
525
|
+
|
|
526
|
+
useEffect(() => {
|
|
527
|
+
if (initialData.length > 0) return
|
|
528
|
+
fetchData().then(setData)
|
|
529
|
+
}, [initialData.length])
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Step 4: Pass Data from Route
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
function YourComponent() {
|
|
537
|
+
const { initialData } = Route.useRouteContext()
|
|
538
|
+
return <YourChildComponent initialData={initialData} />
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## References
|
|
545
|
+
|
|
546
|
+
- [TanStack Router Documentation](https://tanstack.com/router/latest)
|
|
547
|
+
- [TanStack Start Documentation](https://tanstack.com/start/latest)
|
|
548
|
+
- [Cloudflare Workers](https://developers.cloudflare.com/workers/)
|
|
549
|
+
- [Server-Side Rendering Best Practices](https://web.dev/rendering-on-the-web/)
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## Checklist for Implementation
|
|
554
|
+
|
|
555
|
+
- [ ] Use `beforeLoad` (not `loader`)
|
|
556
|
+
- [ ] Add proper TypeScript types for initialData
|
|
557
|
+
- [ ] Handle errors gracefully (don't fail page load)
|
|
558
|
+
- [ ] Pass data through route context
|
|
559
|
+
- [ ] Access with `Route.useRouteContext()`
|
|
560
|
+
- [ ] Initialize component state with SSR data
|
|
561
|
+
- [ ] Skip client-side fetch if SSR data exists
|
|
562
|
+
- [ ] Test with JavaScript disabled
|
|
563
|
+
- [ ] Test hydration (no flash/re-render)
|
|
564
|
+
- [ ] Test real-time updates still work
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
**Status**: Stable - Proven pattern for TanStack Start + Cloudflare Workers
|
|
569
|
+
**Recommendation**: Use for all routes that display user-specific data
|
|
570
|
+
**Last Updated**: 2026-02-21
|
|
571
|
+
**Contributors**: Patrick Michaelsen
|