@decocms/start 0.28.3 → 0.29.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/docs/hydration-and-ssr-migration.md +616 -0
- package/docs/next-steps-tanstack-native.md +333 -0
- package/package.json +8 -2
- package/src/cms/index.ts +1 -0
- package/src/cms/registry.test.ts +118 -0
- package/src/cms/registry.ts +7 -0
- package/src/cms/resolve.test.ts +67 -0
- package/src/cms/resolve.ts +29 -1
- package/src/hooks/DecoPageRenderer.tsx +125 -14
- package/src/middleware/hydrationContext.test.ts +61 -0
- package/src/middleware/hydrationContext.ts +79 -0
- package/src/middleware/index.ts +7 -0
- package/src/middleware/validateSection.test.ts +117 -0
- package/src/middleware/validateSection.ts +91 -0
- package/src/routes/cmsRoute.ts +134 -4
- package/src/routes/components.tsx +21 -3
- package/src/sdk/index.ts +2 -1
- package/src/sdk/useDevice.test.ts +104 -0
- package/src/sdk/useDevice.ts +28 -6
- package/src/sdk/useHydrated.ts +19 -0
- package/src/sdk/useScript.test.ts +53 -0
- package/src/sdk/useScript.ts +49 -1
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
# Hydration & SSR — Migration to TanStack Native Patterns
|
|
2
|
+
|
|
3
|
+
> Migration guide for `@decocms/start` to adopt TanStack Router/Start native SSR, hydration, and deferred data patterns. Eliminates custom server function workarounds and aligns with the framework's execution model.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Current Architecture & Problems](#1-current-architecture--problems)
|
|
10
|
+
2. [TanStack Native Patterns We Should Adopt](#2-tanstack-native-patterns-we-should-adopt)
|
|
11
|
+
3. [Migration Plan](#3-migration-plan)
|
|
12
|
+
4. [Issue-by-Issue Fix Guide](#4-issue-by-issue-fix-guide)
|
|
13
|
+
5. [Testing Checklist](#5-testing-checklist)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Current Architecture & Problems
|
|
18
|
+
|
|
19
|
+
### How it works today
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
┌─────────────────────────────────────────────────────────┐
|
|
23
|
+
│ CMS Page Request │
|
|
24
|
+
│ │
|
|
25
|
+
│ 1. loadCmsPage (GET server function) │
|
|
26
|
+
│ ├── resolves eager sections (Header, Footer, Theme) │
|
|
27
|
+
│ └── extracts deferred section metadata │
|
|
28
|
+
│ │
|
|
29
|
+
│ 2. SSR renders eager sections + skeleton placeholders │
|
|
30
|
+
│ │
|
|
31
|
+
│ 3. Client hydrates, IntersectionObserver fires │
|
|
32
|
+
│ └── loadDeferredSection (POST server function) │
|
|
33
|
+
│ └── resolves section + runs loader │
|
|
34
|
+
│ └── returns enriched props │
|
|
35
|
+
│ │
|
|
36
|
+
│ 4. Client renders the section, fades in │
|
|
37
|
+
└─────────────────────────────────────────────────────────┘
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Problems found in production
|
|
41
|
+
|
|
42
|
+
#### P1: `loadDeferredSection` fails in Cloudflare Workers dev mode
|
|
43
|
+
|
|
44
|
+
**Error:**
|
|
45
|
+
```
|
|
46
|
+
Cannot perform I/O on behalf of a different request.
|
|
47
|
+
I/O objects (such as streams, request/response bodies, and others) created
|
|
48
|
+
in the context of one request handler cannot be accessed from a different
|
|
49
|
+
request's handler. (I/O type: SpanParent)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Root cause:** TanStack Start splits server functions into `?tss-serverfn-split` modules. In Vite dev mode, the Cloudflare worker module runner caches modules per-request. Request A (SSR page load) caches modules with SpanParent I/O objects, Request B (server function POST from client) tries to reuse them — fails because I/O objects are tied to Request A's context.
|
|
53
|
+
|
|
54
|
+
**Impact:** ALL deferred sections fail to load in dev mode. Sites must force sections eager via `alwaysEager`, defeating the purpose of async rendering.
|
|
55
|
+
|
|
56
|
+
#### P2: Eager sections without `registerSectionsSync` render blank
|
|
57
|
+
|
|
58
|
+
When a section is eager (in `resolvedSections`) but NOT registered via `registerSectionsSync()`, `DecoPageRenderer` renders it via `React.lazy` wrapped in `<Suspense fallback={null}>`. During hydration, if the lazy module isn't available synchronously, React unmounts the server HTML and shows the fallback (null) — a blank area.
|
|
59
|
+
|
|
60
|
+
**Why this wasn't noticed before:** Before hydration fixes, script mismatches caused React to do a full client re-render instead of hydration. `React.lazy` works fine on fresh renders, only fails during hydration.
|
|
61
|
+
|
|
62
|
+
#### P3: `useScript(fn)` causes hydration mismatch
|
|
63
|
+
|
|
64
|
+
`useScript(fn)` calls `fn.toString()` + `minifyJs()` to produce inline JavaScript. Vite compiles SSR and client bundles separately — React Compiler transforms may differ, producing different function body strings. Since `dangerouslySetInnerHTML.__html` is checked during hydration, any difference causes:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Warning: A tree hydrated but some attributes of the server rendered HTML
|
|
68
|
+
didn't match the client properties...
|
|
69
|
+
dangerouslySetInnerHTML.__html
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Affected:** `useScriptAsDataURI` has the same issue (it wraps `useScript`).
|
|
73
|
+
|
|
74
|
+
#### P4: Third-party scripts injected into `<head>` break hydration
|
|
75
|
+
|
|
76
|
+
GTM, Emarsys, and similar scripts inject `<script>` elements into `<head>` before React hydration begins. This shifts the DOM tree — React expects the same child count/order that the server rendered, finds extra nodes, and fails hydration.
|
|
77
|
+
|
|
78
|
+
#### P5: N+1 VTEX API calls when all sections are eager
|
|
79
|
+
|
|
80
|
+
When sites force all sections eager (workaround for P1), a typical homepage with 8-12 product shelves fires 16-24+ concurrent VTEX API calls during SSR. This causes:
|
|
81
|
+
- VTEX 503 rate limiting
|
|
82
|
+
- 10+ second SSR times
|
|
83
|
+
- OrderForm/login resolution failures
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 2. TanStack Native Patterns We Should Adopt
|
|
88
|
+
|
|
89
|
+
### 2.1 Deferred Data Loading with `defer()` + `<Await>`
|
|
90
|
+
|
|
91
|
+
**Source:** https://tanstack.com/router/latest/docs/guide/deferred-data-loading
|
|
92
|
+
|
|
93
|
+
TanStack Router has native deferred data support. Instead of a custom server function POST, we can return unawaited promises from the route loader:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
// Route loader
|
|
97
|
+
loader: async () => {
|
|
98
|
+
// Fast: resolve immediately (Header, Footer, Theme)
|
|
99
|
+
const eagerSections = await resolveEagerSections(page);
|
|
100
|
+
|
|
101
|
+
// Slow: don't await — starts resolving, streams when ready
|
|
102
|
+
const deferredSectionsPromise = resolveDeferredSections(page);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
eagerSections,
|
|
106
|
+
deferredSections: deferredSectionsPromise, // unawaited!
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
// Component
|
|
113
|
+
function CmsPage() {
|
|
114
|
+
const { eagerSections, deferredSections } = Route.useLoaderData();
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<>
|
|
118
|
+
{eagerSections.map(s => <SectionRenderer key={s.index} section={s} />)}
|
|
119
|
+
<Await promise={deferredSections} fallback={<SectionSkeletons />}>
|
|
120
|
+
{(sections) => sections.map(s => <SectionRenderer key={s.index} section={s} />)}
|
|
121
|
+
</Await>
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Benefits:**
|
|
128
|
+
- Works in dev mode (no separate server function request)
|
|
129
|
+
- SSR streaming sends skeleton HTML first, then resolved sections
|
|
130
|
+
- Native TanStack cache/invalidation
|
|
131
|
+
- No IntersectionObserver needed for initial load
|
|
132
|
+
|
|
133
|
+
**With React 19 `use()` hook:**
|
|
134
|
+
```tsx
|
|
135
|
+
// React 19 alternative to <Await>
|
|
136
|
+
function DeferredSections({ promise }) {
|
|
137
|
+
const sections = use(promise);
|
|
138
|
+
return sections.map(s => <SectionRenderer key={s.index} section={s} />);
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 2.2 `<ClientOnly>` for Browser-Dependent Components
|
|
143
|
+
|
|
144
|
+
**Source:** https://tanstack.com/router/latest/docs/api/router/clientOnlyComponent
|
|
145
|
+
|
|
146
|
+
For components that use browser APIs or produce non-deterministic output (analytics, GTM, geolocation):
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
import { ClientOnly } from '@tanstack/react-router';
|
|
150
|
+
|
|
151
|
+
// Analytics scripts — no SSR, no hydration mismatch
|
|
152
|
+
function GlobalAnalytics() {
|
|
153
|
+
return (
|
|
154
|
+
<ClientOnly fallback={null}>
|
|
155
|
+
<VtexIsEvents />
|
|
156
|
+
<Sourei gtmId="GTM-XXXXX" />
|
|
157
|
+
</ClientOnly>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**This replaces:** `suppressHydrationWarning`, moving scripts from `<head>` to `<body>`, converting `useScript(fn)` to string constants.
|
|
163
|
+
|
|
164
|
+
### 2.3 `useHydrated` Hook
|
|
165
|
+
|
|
166
|
+
**Source:** TanStack Router execution model
|
|
167
|
+
|
|
168
|
+
For components that need different render output pre/post hydration:
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import { useHydrated } from '@tanstack/react-router';
|
|
172
|
+
|
|
173
|
+
function CartButton() {
|
|
174
|
+
const hydrated = useHydrated();
|
|
175
|
+
|
|
176
|
+
if (!hydrated) {
|
|
177
|
+
// SSR: render loading skeleton
|
|
178
|
+
return <CartSkeleton />;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Client: render interactive cart
|
|
182
|
+
return <InteractiveCart />;
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 2.4 Selective SSR (`ssr: 'data-only'`)
|
|
187
|
+
|
|
188
|
+
**Source:** https://tanstack.com/start/latest/docs/framework/react/guide/selective-ssr
|
|
189
|
+
|
|
190
|
+
For routes where the loader should run on server but the component shouldn't render (shows `pendingComponent` as skeleton):
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
export const Route = createFileRoute('/product/$slug')({
|
|
194
|
+
ssr: 'data-only', // loader runs on server, component renders on client
|
|
195
|
+
pendingComponent: () => <PDPSkeleton />,
|
|
196
|
+
loader: async () => {
|
|
197
|
+
return await loadProductData(); // runs server-side
|
|
198
|
+
},
|
|
199
|
+
component: ProductPage, // renders client-side only
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Use cases:**
|
|
204
|
+
- PDP with complex client-side interactions (image zoom, variant selector)
|
|
205
|
+
- Pages with lots of `useEffect` dependencies
|
|
206
|
+
|
|
207
|
+
### 2.5 `createIsomorphicFn` for Environment-Specific Logic
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
import { createIsomorphicFn } from '@tanstack/react-start';
|
|
211
|
+
|
|
212
|
+
const getDeviceInfo = createIsomorphicFn()
|
|
213
|
+
.server(() => ({ source: 'cf-headers', device: getDeviceFromHeaders() }))
|
|
214
|
+
.client(() => ({ source: 'window', device: getDeviceFromWindow() }));
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 2.6 Loaders Are Isomorphic (Critical Understanding)
|
|
218
|
+
|
|
219
|
+
**Route loaders run on BOTH server (SSR) and client (SPA navigation).** They are NOT server-only.
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
// ❌ Wrong assumption: loader is server-only
|
|
223
|
+
loader: () => {
|
|
224
|
+
const secret = process.env.API_KEY; // EXPOSED to client bundle
|
|
225
|
+
return fetch(`/api?key=${secret}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ✅ Correct: use server function for server-only operations
|
|
229
|
+
const fetchSecurely = createServerFn().handler(() => {
|
|
230
|
+
const secret = process.env.API_KEY; // server-only
|
|
231
|
+
return fetch(`/api?key=${secret}`);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
loader: () => fetchSecurely() // isomorphic call
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Implication for `@decocms/start`:** The `loadCmsPage` server function is correct — it's called from the loader and executes server-side. But CMS section loaders that access server-only resources (KV, D1) must also be wrapped in server functions.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 3. Migration Plan
|
|
242
|
+
|
|
243
|
+
### Phase 1: Fix Hydration Mismatches (site-level, no framework changes)
|
|
244
|
+
|
|
245
|
+
| Task | Pattern | Files |
|
|
246
|
+
|------|---------|-------|
|
|
247
|
+
| Wrap analytics in `<ClientOnly>` | 2.2 | `GlobalAnalytics.tsx`, `Sourei.tsx` |
|
|
248
|
+
| Fix invalid HTML (`<span>` in `<option>`) | Standard React | `Sort.tsx` |
|
|
249
|
+
| Fix `selected` on `<option>` → `defaultValue` | Standard React | `Sort.tsx` |
|
|
250
|
+
| Move third-party scripts out of `<head>` | 2.2 | `__root.tsx` |
|
|
251
|
+
|
|
252
|
+
### Phase 2: Replace `useScript(fn)` with safe alternatives (framework)
|
|
253
|
+
|
|
254
|
+
| Task | Pattern | Files |
|
|
255
|
+
|------|---------|-------|
|
|
256
|
+
| Deprecate `useScript(fn)` | 2.2 | `sdk/useScript.ts` |
|
|
257
|
+
| Add `inlineScript(str)` helper | New utility | `sdk/useScript.ts` |
|
|
258
|
+
| Add dev warning when `fn.toString()` differs | DX improvement | `sdk/useScript.ts` |
|
|
259
|
+
| Document string constant pattern | Docs | This file |
|
|
260
|
+
|
|
261
|
+
**New helper:**
|
|
262
|
+
```tsx
|
|
263
|
+
// sdk/useScript.ts
|
|
264
|
+
|
|
265
|
+
/** @deprecated Use plain string constants with dangerouslySetInnerHTML instead.
|
|
266
|
+
* fn.toString() produces different output in SSR vs client Vite builds,
|
|
267
|
+
* causing hydration mismatches. */
|
|
268
|
+
export function useScript(fn: Function, ...args: unknown[]): string { ... }
|
|
269
|
+
|
|
270
|
+
/** Safe inline script — returns props for <script> element. */
|
|
271
|
+
export function inlineScript(js: string) {
|
|
272
|
+
return { dangerouslySetInnerHTML: { __html: js } } as const;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Phase 3: Adopt `defer()` + `<Await>` for deferred sections (framework)
|
|
277
|
+
|
|
278
|
+
This is the biggest change. Replace the custom `loadDeferredSection` POST server function with TanStack Router's native deferred data loading.
|
|
279
|
+
|
|
280
|
+
#### 3.1 Change `cmsRoute.ts` loader to return deferred promises
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
// BEFORE (current)
|
|
284
|
+
loader: async () => {
|
|
285
|
+
const page = await loadCmsPage({ data: { path, searchParams } });
|
|
286
|
+
return {
|
|
287
|
+
resolvedSections: page.resolvedSections, // eager sections
|
|
288
|
+
deferredSections: page.deferredSections, // metadata only
|
|
289
|
+
// client must call loadDeferredSection() POST to resolve each one
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// AFTER (native deferred)
|
|
294
|
+
loader: async () => {
|
|
295
|
+
const page = await loadCmsPage({ data: { path, searchParams } });
|
|
296
|
+
|
|
297
|
+
// Start resolving deferred sections NOW but don't await
|
|
298
|
+
const deferredPromise = resolveDeferredSectionsInParallel(
|
|
299
|
+
page.deferredSections, page.pagePath, page.pageUrl
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
resolvedSections: page.resolvedSections, // eager — awaited
|
|
304
|
+
deferredSections: deferredPromise, // deferred — streaming!
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
#### 3.2 Change `DecoPageRenderer` to use `<Await>`
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
// BEFORE (current)
|
|
313
|
+
function DecoPageRenderer({ resolvedSections, deferredSections, loadDeferredSectionFn }) {
|
|
314
|
+
const merged = mergeSections(resolvedSections, deferredSections);
|
|
315
|
+
return merged.map(section =>
|
|
316
|
+
section.type === 'deferred'
|
|
317
|
+
? <DeferredSectionWrapper ... loadFn={loadDeferredSectionFn} />
|
|
318
|
+
: <EagerSectionWrapper ... />
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// AFTER (native deferred)
|
|
323
|
+
function DecoPageRenderer({ resolvedSections, deferredSectionsPromise }) {
|
|
324
|
+
return (
|
|
325
|
+
<>
|
|
326
|
+
{resolvedSections.map(s => <SectionRenderer key={s.index} section={s} />)}
|
|
327
|
+
<Await
|
|
328
|
+
promise={deferredSectionsPromise}
|
|
329
|
+
fallback={<DeferredSkeletons sections={deferredSections} />}
|
|
330
|
+
>
|
|
331
|
+
{(resolved) => resolved.map(s => <SectionRenderer key={s.index} section={s} />)}
|
|
332
|
+
</Await>
|
|
333
|
+
</>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### 3.3 Keep IntersectionObserver as optimization (optional)
|
|
339
|
+
|
|
340
|
+
For below-the-fold deferred sections, we can still use IntersectionObserver to delay client-side rendering until scroll. But the data is already loaded (streamed) — we just defer the React render.
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
function LazyRenderSection({ section }) {
|
|
344
|
+
const [visible, setVisible] = useState(false);
|
|
345
|
+
const ref = useRef(null);
|
|
346
|
+
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
const io = new IntersectionObserver(
|
|
349
|
+
([entry]) => { if (entry.isIntersecting) setVisible(true); },
|
|
350
|
+
{ rootMargin: '300px' }
|
|
351
|
+
);
|
|
352
|
+
if (ref.current) io.observe(ref.current);
|
|
353
|
+
return () => io.disconnect();
|
|
354
|
+
}, []);
|
|
355
|
+
|
|
356
|
+
if (!visible) return <div ref={ref}><SectionSkeleton section={section} /></div>;
|
|
357
|
+
return <SectionRenderer section={section} />;
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Phase 4: Add `<ClientOnly>` support for section registration (framework)
|
|
362
|
+
|
|
363
|
+
Allow sections to declare they're client-only:
|
|
364
|
+
|
|
365
|
+
```tsx
|
|
366
|
+
// setup.ts
|
|
367
|
+
registerSectionsSync({
|
|
368
|
+
"site/sections/Sourei/Sourei.tsx": SoureiModule,
|
|
369
|
+
}, { clientOnly: true }); // wraps in <ClientOnly> automatically
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Or per-section:
|
|
373
|
+
```tsx
|
|
374
|
+
registerSection("site/sections/Sourei/Sourei.tsx", SoureiModule, {
|
|
375
|
+
clientOnly: true,
|
|
376
|
+
loadingFallback: () => null,
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Phase 5: Add dev warnings for common mistakes (framework)
|
|
381
|
+
|
|
382
|
+
```tsx
|
|
383
|
+
// DecoPageRenderer.tsx — warn if eager section is not sync-registered
|
|
384
|
+
if (import.meta.env.DEV && !getSyncComponent(section.component)) {
|
|
385
|
+
console.warn(
|
|
386
|
+
`[DecoPageRenderer] Eager section "${section.component}" is not in registerSectionsSync(). ` +
|
|
387
|
+
`This will cause blank content during hydration. Add it to registerSectionsSync() in setup.ts.`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// useScript.ts — warn about fn.toString() risk
|
|
392
|
+
if (import.meta.env.DEV) {
|
|
393
|
+
const ssrStr = fn.toString();
|
|
394
|
+
console.warn(
|
|
395
|
+
`[useScript] Using fn.toString() for "${fn.name || 'anonymous'}". ` +
|
|
396
|
+
`This may produce different output in SSR vs client builds. ` +
|
|
397
|
+
`Consider using a plain string constant instead.`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## 4. Issue-by-Issue Fix Guide
|
|
405
|
+
|
|
406
|
+
### P1 Fix: Server function I/O error → Use `defer()` (Phase 3)
|
|
407
|
+
|
|
408
|
+
**Before:** Client makes POST to `loadDeferredSection` → separate request → I/O error
|
|
409
|
+
**After:** Deferred sections resolve in the SAME request via `defer()` → streamed to client
|
|
410
|
+
|
|
411
|
+
No separate server function request = no cross-request I/O issue.
|
|
412
|
+
|
|
413
|
+
### P2 Fix: Blank eager sections → Dev warning + `syncThenable` fallback (Phase 5)
|
|
414
|
+
|
|
415
|
+
**Quick fix:** Warning in dev mode when an eager section isn't sync-registered.
|
|
416
|
+
|
|
417
|
+
**Proper fix:** In `DecoPageRenderer`, for eager sections without sync registration, create a `syncThenable` from the server-resolved component module instead of using bare `React.lazy`:
|
|
418
|
+
|
|
419
|
+
```tsx
|
|
420
|
+
// If the component was resolved on the server, pre-populate the lazy cache
|
|
421
|
+
// with a syncThenable so hydration doesn't trigger Suspense
|
|
422
|
+
const resolvedModule = getResolvedComponent(section.component);
|
|
423
|
+
if (resolvedModule) {
|
|
424
|
+
const syncLazy = React.lazy(() => syncThenable({ default: resolvedModule }));
|
|
425
|
+
// This won't trigger Suspense during hydration
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### P3 Fix: `useScript(fn)` mismatch → Deprecate + `inlineScript()` helper (Phase 2)
|
|
430
|
+
|
|
431
|
+
**Site-level workaround (now):** Convert to plain string constants.
|
|
432
|
+
**Framework fix (Phase 2):** Deprecate `useScript(fn)`, add `inlineScript(str)` helper.
|
|
433
|
+
|
|
434
|
+
### P4 Fix: Third-party scripts in head → `<ClientOnly>` (Phase 1)
|
|
435
|
+
|
|
436
|
+
**Site-level:** Wrap analytics/GTM in `<ClientOnly fallback={null}>`.
|
|
437
|
+
**Framework:** Add `clientOnly` option to section registration (Phase 4).
|
|
438
|
+
|
|
439
|
+
### P5 Fix: N+1 VTEX calls → Concurrency limiter + keep shelves deferred (Phase 3)
|
|
440
|
+
|
|
441
|
+
With `defer()`, deferred sections resolve server-side but stream progressively. Add a concurrency limiter:
|
|
442
|
+
|
|
443
|
+
```tsx
|
|
444
|
+
// sdk/concurrency.ts
|
|
445
|
+
export function createConcurrencyLimiter(max: number) {
|
|
446
|
+
let inflight = 0;
|
|
447
|
+
const queue: Array<() => void> = [];
|
|
448
|
+
|
|
449
|
+
return async function limit<T>(fn: () => Promise<T>): Promise<T> {
|
|
450
|
+
if (inflight >= max) {
|
|
451
|
+
await new Promise<void>(resolve => queue.push(resolve));
|
|
452
|
+
}
|
|
453
|
+
inflight++;
|
|
454
|
+
try {
|
|
455
|
+
return await fn();
|
|
456
|
+
} finally {
|
|
457
|
+
inflight--;
|
|
458
|
+
queue.shift()?.();
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Usage in VTEX fetch
|
|
464
|
+
const vtexLimit = createConcurrencyLimiter(6);
|
|
465
|
+
const response = await vtexLimit(() => fetch(vtexUrl));
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## 5. Testing Checklist
|
|
471
|
+
|
|
472
|
+
### Hydration
|
|
473
|
+
|
|
474
|
+
- [ ] No `dangerouslySetInnerHTML.__html` mismatch warnings in console
|
|
475
|
+
- [ ] No "hydration failed" React warnings
|
|
476
|
+
- [ ] Server HTML matches client render (inspect source vs DOM)
|
|
477
|
+
- [ ] `suppressHydrationWarning` only on `<html>` and `<body>` (not as a blanket fix)
|
|
478
|
+
|
|
479
|
+
### Deferred Sections
|
|
480
|
+
|
|
481
|
+
- [ ] Skeletons show immediately on page load
|
|
482
|
+
- [ ] Deferred content appears progressively (not all at once)
|
|
483
|
+
- [ ] Back/forward navigation shows cached content instantly
|
|
484
|
+
- [ ] SPA navigation to PLP shows skeleton → products
|
|
485
|
+
- [ ] SearchResult preserves URL params (filters, sort, pagination) after hydration
|
|
486
|
+
|
|
487
|
+
### Performance
|
|
488
|
+
|
|
489
|
+
- [ ] SSR time < 3s for homepage (with shelves deferred)
|
|
490
|
+
- [ ] SSR time < 2s for PLP (SearchResult deferred or eager with fast VTEX)
|
|
491
|
+
- [ ] No VTEX 503 errors during SSR
|
|
492
|
+
- [ ] CLS < 0.1 (skeletons match final content dimensions)
|
|
493
|
+
- [ ] FCP < 1.5s (eager sections render immediately)
|
|
494
|
+
|
|
495
|
+
### Dev Mode
|
|
496
|
+
|
|
497
|
+
- [ ] Deferred sections load in dev mode (no I/O error)
|
|
498
|
+
- [ ] HMR works for section components
|
|
499
|
+
- [ ] Console shows helpful warnings for misconfigured sections
|
|
500
|
+
|
|
501
|
+
### Edge Cases
|
|
502
|
+
|
|
503
|
+
- [ ] Bot/crawler gets full HTML (no deferred skeletons)
|
|
504
|
+
- [ ] JavaScript disabled: eager sections visible, deferred shows skeleton
|
|
505
|
+
- [ ] Slow network: skeleton persists, no blank flash
|
|
506
|
+
- [ ] Multiple deferred sections on same page all resolve
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## 6. TanStack CLI — Querying Official Docs
|
|
511
|
+
|
|
512
|
+
The `@tanstack/cli` package provides direct access to official TanStack documentation from the terminal. Use it to research implementation details, find examples, and verify patterns before coding.
|
|
513
|
+
|
|
514
|
+
### Installation
|
|
515
|
+
|
|
516
|
+
```bash
|
|
517
|
+
npm install -g @tanstack/cli
|
|
518
|
+
# or use without installing:
|
|
519
|
+
npx -y @tanstack/cli <command>
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Key Commands
|
|
523
|
+
|
|
524
|
+
#### Search docs by topic
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
# Search across a specific library
|
|
528
|
+
tanstack search-docs "server functions" --library start --json
|
|
529
|
+
tanstack search-docs "hydration SSR" --library start --json
|
|
530
|
+
tanstack search-docs "deferred data streaming" --library router --json
|
|
531
|
+
tanstack search-docs "ClientOnly" --library router --json
|
|
532
|
+
tanstack search-docs "createServerFn middleware" --library start --json
|
|
533
|
+
|
|
534
|
+
# Useful searches for this migration:
|
|
535
|
+
tanstack search-docs "Await defer" --library router --json
|
|
536
|
+
tanstack search-docs "useHydrated" --library router --json
|
|
537
|
+
tanstack search-docs "Selective SSR data-only" --library start --json
|
|
538
|
+
tanstack search-docs "code execution patterns" --library start --json
|
|
539
|
+
tanstack search-docs "external data loading" --library router --json
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
#### Read a specific doc page
|
|
543
|
+
|
|
544
|
+
```bash
|
|
545
|
+
# Format: tanstack doc <library> <path> --json
|
|
546
|
+
tanstack doc start framework/react/guide/hydration-errors --json
|
|
547
|
+
tanstack doc start framework/react/guide/selective-ssr --json
|
|
548
|
+
tanstack doc start framework/react/guide/execution-model --json
|
|
549
|
+
tanstack doc start framework/react/guide/server-functions --json
|
|
550
|
+
tanstack doc start framework/react/guide/middleware --json
|
|
551
|
+
tanstack doc router guide/deferred-data-loading --json
|
|
552
|
+
tanstack doc router guide/ssr --json
|
|
553
|
+
tanstack doc router guide/external-data-loading --json
|
|
554
|
+
tanstack doc router guide/data-loading --json
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
#### List available libraries
|
|
558
|
+
|
|
559
|
+
```bash
|
|
560
|
+
tanstack libraries --json
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
#### Explore ecosystem tools
|
|
564
|
+
|
|
565
|
+
```bash
|
|
566
|
+
tanstack ecosystem --category database --json
|
|
567
|
+
tanstack ecosystem --category auth --json
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Recommended Research Workflow
|
|
571
|
+
|
|
572
|
+
When implementing a phase of this migration:
|
|
573
|
+
|
|
574
|
+
```bash
|
|
575
|
+
# 1. Search for the topic
|
|
576
|
+
tanstack search-docs "defer Await streaming" --library router --json
|
|
577
|
+
|
|
578
|
+
# 2. Read the most relevant result
|
|
579
|
+
tanstack doc router guide/deferred-data-loading --json
|
|
580
|
+
|
|
581
|
+
# 3. Check for related patterns in Start
|
|
582
|
+
tanstack search-docs "streaming SSR" --library start --json
|
|
583
|
+
|
|
584
|
+
# 4. Look for API reference
|
|
585
|
+
tanstack doc router api/router/awaitComponent --json
|
|
586
|
+
|
|
587
|
+
# 5. Check for breaking changes or version-specific notes
|
|
588
|
+
tanstack search-docs "migration breaking changes" --library start --json
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Key Doc Pages for This Migration
|
|
592
|
+
|
|
593
|
+
| Phase | Doc Page | Command |
|
|
594
|
+
|-------|----------|---------|
|
|
595
|
+
| Phase 1 | Hydration Errors | `tanstack doc start framework/react/guide/hydration-errors --json` |
|
|
596
|
+
| Phase 2 | Code Execution Patterns | `tanstack doc start framework/react/guide/code-execution-patterns --json` |
|
|
597
|
+
| Phase 3 | Deferred Data Loading | `tanstack doc router guide/deferred-data-loading --json` |
|
|
598
|
+
| Phase 3 | SSR Streaming | `tanstack doc router guide/ssr --json` |
|
|
599
|
+
| Phase 3 | External Data Loading | `tanstack doc router guide/external-data-loading --json` |
|
|
600
|
+
| Phase 4 | ClientOnly Component | `tanstack doc router api/router/clientOnlyComponent --json` |
|
|
601
|
+
| Phase 4 | Execution Model | `tanstack doc start framework/react/guide/execution-model --json` |
|
|
602
|
+
| All | Server Functions | `tanstack doc start framework/react/guide/server-functions --json` |
|
|
603
|
+
| All | Middleware | `tanstack doc start framework/react/guide/middleware --json` |
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
## References
|
|
608
|
+
|
|
609
|
+
- [TanStack Router — Deferred Data Loading](https://tanstack.com/router/latest/docs/guide/deferred-data-loading)
|
|
610
|
+
- [TanStack Start — Hydration Errors](https://tanstack.com/start/latest/docs/framework/react/guide/hydration-errors)
|
|
611
|
+
- [TanStack Start — Selective SSR](https://tanstack.com/start/latest/docs/framework/react/guide/selective-ssr)
|
|
612
|
+
- [TanStack Start — Execution Model](https://tanstack.com/start/latest/docs/framework/react/guide/execution-model)
|
|
613
|
+
- [TanStack Router — ClientOnly Component](https://tanstack.com/router/latest/docs/api/router/clientOnlyComponent)
|
|
614
|
+
- [TanStack Router — SSR Guide](https://tanstack.com/router/latest/docs/guide/ssr)
|
|
615
|
+
- [Cloudflare Workers — Cross-Request I/O](https://developers.cloudflare.com/workers/runtime-apis/context/)
|
|
616
|
+
- [TanStack CLI (`@tanstack/cli`)](https://www.npmjs.com/package/@tanstack/cli) — Query official docs from the terminal
|