0
# SSR Hydration
1
2
Server-side rendering support with hydration capabilities for fast initial page loads and SEO optimization, enabling isomorphic application development.
3
4
## Capabilities
5
6
### Hydration Markup
7
8
Utilities for generating and processing hydration markers that enable client-side hydration of server-rendered content.
9
10
```typescript { .api }
11
/**
12
* Utilities for hydration markup generation and processing
13
*/
14
const HydrationMarkup: {
15
/** The attribute name used to mark hydratable elements */
16
readonly attributeMarkerName: string;
17
18
/**
19
* Creates a start marker for content binding hydration
20
* @param index - The binding index
21
* @param targetNodeId - The ID of the target node
22
* @returns HTML comment marker string
23
*/
24
contentBindingStartMarker(index: number, targetNodeId: string): string;
25
26
/**
27
* Creates an end marker for content binding hydration
28
* @param index - The binding index
29
* @returns HTML comment marker string
30
*/
31
contentBindingEndMarker(index: number): string;
32
33
/**
34
* Creates a marker for attribute binding hydration
35
* @param index - The binding index
36
* @param targetNodeId - The ID of the target node
37
* @param attributeName - The name of the attribute
38
* @returns HTML comment marker string
39
*/
40
attributeBindingMarker(index: number, targetNodeId: string, attributeName: string): string;
41
42
/**
43
* Parses hydration markers from HTML content
44
* @param html - The HTML content to parse
45
* @returns Parsed hydration information
46
*/
47
parseHydrationMarkers(html: string): HydrationInfo;
48
};
49
50
/**
51
* Hydration information extracted from markers
52
*/
53
interface HydrationInfo {
54
/** Content binding locations */
55
contentBindings: Array<{
56
index: number;
57
targetNodeId: string;
58
startNode: Comment;
59
endNode: Comment;
60
}>;
61
62
/** Attribute binding locations */
63
attributeBindings: Array<{
64
index: number;
65
targetNodeId: string;
66
attributeName: string;
67
marker: Comment;
68
}>;
69
70
/** Hydratable elements */
71
hydratableElements: Array<{
72
element: Element;
73
templateId: string;
74
}>;
75
}
76
```
77
78
**Usage Examples:**
79
80
```typescript
81
import { ViewTemplate, html } from "@microsoft/fast-element";
82
import { HydrationMarkup } from "@microsoft/fast-element/element-hydration.js";
83
84
// Template that supports hydration
85
const userCardTemplate = html<UserCard>`
86
<div class="user-card" ${HydrationMarkup.attributeMarker('data-user-id', x => x.userId)}>
87
${HydrationMarkup.contentBindingStartMarker(0, 'user-name')}
88
<h2 id="user-name">${x => x.name}</h2>
89
${HydrationMarkup.contentBindingEndMarker(0)}
90
91
${HydrationMarkup.contentBindingStartMarker(1, 'user-email')}
92
<p id="user-email">${x => x.email}</p>
93
${HydrationMarkup.contentBindingEndMarker(1)}
94
95
<div class="user-stats">
96
${HydrationMarkup.contentBindingStartMarker(2, 'post-count')}
97
<span id="post-count">Posts: ${x => x.postCount}</span>
98
${HydrationMarkup.contentBindingEndMarker(2)}
99
100
${HydrationMarkup.contentBindingStartMarker(3, 'follower-count')}
101
<span id="follower-count">Followers: ${x => x.followerCount}</span>
102
${HydrationMarkup.contentBindingEndMarker(3)}
103
</div>
104
</div>
105
`;
106
107
// Server-side rendering function
108
function renderUserCardSSR(userData: UserData): string {
109
// Server-side template rendering with hydration markers
110
let html = `
111
<div class="user-card" data-user-id="${userData.userId}" ${HydrationMarkup.attributeMarkerName}="user-card-template">
112
${HydrationMarkup.contentBindingStartMarker(0, 'user-name')}
113
<h2 id="user-name">${userData.name}</h2>
114
${HydrationMarkup.contentBindingEndMarker(0)}
115
116
${HydrationMarkup.contentBindingStartMarker(1, 'user-email')}
117
<p id="user-email">${userData.email}</p>
118
${HydrationMarkup.contentBindingEndMarker(1)}
119
120
<div class="user-stats">
121
${HydrationMarkup.contentBindingStartMarker(2, 'post-count')}
122
<span id="post-count">Posts: ${userData.postCount}</span>
123
${HydrationMarkup.contentBindingEndMarker(2)}
124
125
${HydrationMarkup.contentBindingStartMarker(3, 'follower-count')}
126
<span id="follower-count">Followers: ${userData.followerCount}</span>
127
${HydrationMarkup.contentBindingEndMarker(3)}
128
</div>
129
</div>
130
`;
131
132
return html;
133
}
134
135
// Client-side hydration process
136
class HydrationManager {
137
static hydrateUserCard(element: Element, userData: UserData): UserCard {
138
// Parse hydration info
139
const hydrationInfo = HydrationMarkup.parseHydrationMarkers(element.outerHTML);
140
141
// Create component instance
142
const userCard = new UserCard();
143
userCard.userId = userData.userId;
144
userCard.name = userData.name;
145
userCard.email = userData.email;
146
userCard.postCount = userData.postCount;
147
userCard.followerCount = userData.followerCount;
148
149
// Hydrate the element
150
this.hydrateElement(userCard, element, hydrationInfo);
151
152
return userCard;
153
}
154
155
private static hydrateElement(
156
component: any,
157
element: Element,
158
hydrationInfo: HydrationInfo
159
): void {
160
// Process content bindings
161
hydrationInfo.contentBindings.forEach(binding => {
162
const targetElement = element.querySelector(`#${binding.targetNodeId}`);
163
if (targetElement) {
164
// Establish reactive binding for content
165
this.establishContentBinding(component, targetElement, binding.index);
166
}
167
});
168
169
// Process attribute bindings
170
hydrationInfo.attributeBindings.forEach(binding => {
171
const targetElement = element.querySelector(`#${binding.targetNodeId}`);
172
if (targetElement) {
173
// Establish reactive binding for attribute
174
this.establishAttributeBinding(
175
component,
176
targetElement,
177
binding.attributeName,
178
binding.index
179
);
180
}
181
});
182
}
183
184
private static establishContentBinding(
185
component: any,
186
element: Element,
187
bindingIndex: number
188
): void {
189
// Set up reactive content binding
190
Observable.getNotifier(component).subscribe({
191
handleChange: () => {
192
// Update content based on component state
193
this.updateElementContent(component, element, bindingIndex);
194
}
195
});
196
}
197
198
private static establishAttributeBinding(
199
component: any,
200
element: Element,
201
attributeName: string,
202
bindingIndex: number
203
): void {
204
// Set up reactive attribute binding
205
Observable.getNotifier(component).subscribe({
206
handleChange: () => {
207
// Update attribute based on component state
208
this.updateElementAttribute(component, element, attributeName, bindingIndex);
209
}
210
});
211
}
212
213
private static updateElementContent(
214
component: any,
215
element: Element,
216
bindingIndex: number
217
): void {
218
// Update logic based on binding index and component state
219
switch (bindingIndex) {
220
case 0: // User name
221
element.textContent = component.name;
222
break;
223
case 1: // User email
224
element.textContent = component.email;
225
break;
226
case 2: // Post count
227
element.textContent = `Posts: ${component.postCount}`;
228
break;
229
case 3: // Follower count
230
element.textContent = `Followers: ${component.followerCount}`;
231
break;
232
}
233
}
234
235
private static updateElementAttribute(
236
component: any,
237
element: Element,
238
attributeName: string,
239
bindingIndex: number
240
): void {
241
// Update attribute based on component state
242
if (attributeName === 'data-user-id') {
243
element.setAttribute(attributeName, component.userId);
244
}
245
}
246
}
247
248
interface UserData {
249
userId: string;
250
name: string;
251
email: string;
252
postCount: number;
253
followerCount: number;
254
}
255
256
class UserCard {
257
userId: string = '';
258
name: string = '';
259
email: string = '';
260
postCount: number = 0;
261
followerCount: number = 0;
262
}
263
```
264
265
### Hydratable Templates
266
267
Template interfaces and implementations that support server-side rendering and client-side hydration.
268
269
```typescript { .api }
270
/**
271
* Checks if an object supports hydration
272
* @param obj - The object to check
273
* @returns True if the object is hydratable
274
*/
275
function isHydratable(obj: any): obj is { [Hydratable]: true };
276
277
/**
278
* Symbol that marks an object as hydratable
279
*/
280
const Hydratable: unique symbol;
281
282
/**
283
* Element view template that supports hydration
284
*/
285
interface HydratableElementViewTemplate<TSource = any, TParent = any>
286
extends ElementViewTemplate<TSource, TParent> {
287
288
/**
289
* Hydrates an element view from server-rendered content
290
* @param firstChild - First child node of the hydration range
291
* @param lastChild - Last child node of the hydration range
292
* @param hostBindingTarget - The element that host behaviors will be bound to
293
*/
294
hydrate(
295
firstChild: Node,
296
lastChild: Node,
297
hostBindingTarget?: Element
298
): ElementView<TSource, TParent>;
299
}
300
301
/**
302
* Synthetic view template that supports hydration
303
*/
304
interface HydratableSyntheticViewTemplate<TSource = any, TParent = any>
305
extends SyntheticViewTemplate<TSource, TParent> {
306
307
/**
308
* Hydrates a synthetic view from server-rendered content
309
* @param firstChild - First child node of the hydration range
310
* @param lastChild - Last child node of the hydration range
311
*/
312
hydrate(firstChild: Node, lastChild: Node): SyntheticView<TSource, TParent>;
313
}
314
315
/**
316
* View interface that supports hydration
317
*/
318
interface HydratableView extends HTMLView {
319
/** Indicates this view supports hydration */
320
readonly isHydratable: true;
321
322
/**
323
* Hydrates the view from existing DOM content
324
* @param firstChild - First child node to hydrate
325
* @param lastChild - Last child node to hydrate
326
*/
327
hydrate(firstChild: Node, lastChild: Node): void;
328
}
329
```
330
331
**Usage Examples:**
332
333
```typescript
334
import {
335
isHydratable,
336
Hydratable,
337
HydratableElementViewTemplate,
338
ViewTemplate,
339
html,
340
FASTElement,
341
customElement
342
} from "@microsoft/fast-element";
343
344
// Create hydratable template
345
function createHydratableTemplate<T>(): HydratableElementViewTemplate<T> {
346
const template = html<T>`
347
<div class="hydratable-content">
348
<h1>${x => (x as any).title}</h1>
349
<p>${x => (x as any).description}</p>
350
</div>
351
`;
352
353
// Mark template as hydratable
354
(template as any)[Hydratable] = true;
355
356
// Add hydration method
357
(template as any).hydrate = function(
358
firstChild: Node,
359
lastChild: Node,
360
hostBindingTarget?: Element
361
) {
362
// Create view from existing DOM
363
const view = this.create(hostBindingTarget);
364
365
// Hydrate from existing nodes
366
view.hydrate(firstChild, lastChild);
367
368
return view;
369
};
370
371
return template as HydratableElementViewTemplate<T>;
372
}
373
374
// Hydratable component
375
@customElement("hydratable-component")
376
export class HydratableComponent extends FASTElement {
377
title: string = "Default Title";
378
description: string = "Default Description";
379
380
// Use hydratable template
381
static template = createHydratableTemplate<HydratableComponent>();
382
383
// Support SSR hydration
384
static supportsHydration = true;
385
386
connectedCallback() {
387
super.connectedCallback();
388
389
// Check if this component needs hydration
390
if (this.needsHydration()) {
391
this.performHydration();
392
}
393
}
394
395
private needsHydration(): boolean {
396
// Check for hydration markers or server-rendered content
397
return this.hasAttribute('data-ssr') ||
398
this.querySelector('[data-hydration-marker]') !== null;
399
}
400
401
private performHydration(): void {
402
if (isHydratable(HydratableComponent.template)) {
403
// Get existing content range
404
const firstChild = this.shadowRoot?.firstChild;
405
const lastChild = this.shadowRoot?.lastChild;
406
407
if (firstChild && lastChild) {
408
// Perform hydration
409
const view = HydratableComponent.template.hydrate(
410
firstChild,
411
lastChild,
412
this
413
);
414
415
// Bind to current component data
416
view.bind(this);
417
}
418
}
419
}
420
}
421
422
// SSR rendering utility
423
class SSRRenderer {
424
static renderComponentToString<T>(
425
template: HydratableElementViewTemplate<T>,
426
data: T
427
): string {
428
if (!isHydratable(template)) {
429
throw new Error('Template must be hydratable for SSR');
430
}
431
432
// Server-side rendering logic
433
return this.renderTemplateWithData(template, data);
434
}
435
436
private static renderTemplateWithData<T>(
437
template: HydratableElementViewTemplate<T>,
438
data: T
439
): string {
440
// Simplified SSR rendering
441
// In real implementation, this would involve proper template processing
442
443
const title = (data as any).title || 'Default Title';
444
const description = (data as any).description || 'Default Description';
445
446
return `
447
<div class="hydratable-content" data-ssr="true">
448
<!-- ${HydrationMarkup.contentBindingStartMarker(0, 'title')} -->
449
<h1>${title}</h1>
450
<!-- ${HydrationMarkup.contentBindingEndMarker(0)} -->
451
452
<!-- ${HydrationMarkup.contentBindingStartMarker(1, 'description')} -->
453
<p>${description}</p>
454
<!-- ${HydrationMarkup.contentBindingEndMarker(1)} -->
455
</div>
456
`;
457
}
458
}
459
460
// Client-side hydration orchestrator
461
class HydrationOrchestrator {
462
private hydrationQueue: Array<{
463
element: Element;
464
component: any;
465
template: HydratableElementViewTemplate<any>;
466
}> = [];
467
468
queueForHydration<T>(
469
element: Element,
470
component: T,
471
template: HydratableElementViewTemplate<T>
472
): void {
473
if (!isHydratable(template)) {
474
console.warn('Template is not hydratable, skipping hydration queue');
475
return;
476
}
477
478
this.hydrationQueue.push({ element, component, template });
479
}
480
481
async processHydrationQueue(): Promise<void> {
482
for (const item of this.hydrationQueue) {
483
try {
484
await this.hydrateComponent(item);
485
} catch (error) {
486
console.error('Hydration failed for component:', error);
487
}
488
}
489
490
this.hydrationQueue = [];
491
}
492
493
private async hydrateComponent(item: {
494
element: Element;
495
component: any;
496
template: HydratableElementViewTemplate<any>;
497
}): Promise<void> {
498
const { element, component, template } = item;
499
500
// Find hydration range
501
const shadowRoot = element.shadowRoot;
502
if (!shadowRoot) {
503
throw new Error('Element must have shadow root for hydration');
504
}
505
506
const firstChild = shadowRoot.firstChild;
507
const lastChild = shadowRoot.lastChild;
508
509
if (firstChild && lastChild) {
510
// Perform hydration
511
const view = template.hydrate(firstChild, lastChild, element);
512
513
// Bind to component data
514
view.bind(component);
515
516
// Mark as hydrated
517
element.setAttribute('data-hydrated', 'true');
518
}
519
}
520
}
521
522
// Application setup with SSR hydration
523
class SSRApp {
524
private hydrationOrchestrator = new HydrationOrchestrator();
525
526
async initializeWithHydration(): Promise<void> {
527
// Find all server-rendered components
528
const ssrElements = document.querySelectorAll('[data-ssr]');
529
530
for (const element of ssrElements) {
531
await this.setupComponentHydration(element);
532
}
533
534
// Process all queued hydrations
535
await this.hydrationOrchestrator.processHydrationQueue();
536
}
537
538
private async setupComponentHydration(element: Element): Promise<void> {
539
const tagName = element.tagName.toLowerCase();
540
541
// Map element to component class and template
542
const componentInfo = this.getComponentInfo(tagName);
543
if (!componentInfo) {
544
return;
545
}
546
547
const { componentClass, template } = componentInfo;
548
549
// Create component instance
550
const component = new componentClass();
551
552
// Extract data from SSR attributes or content
553
this.populateComponentFromSSR(component, element);
554
555
// Queue for hydration
556
this.hydrationOrchestrator.queueForHydration(
557
element,
558
component,
559
template
560
);
561
}
562
563
private getComponentInfo(tagName: string): {
564
componentClass: any;
565
template: HydratableElementViewTemplate<any>;
566
} | null {
567
// Component registry lookup
568
const registry: Record<string, any> = {
569
'hydratable-component': {
570
componentClass: HydratableComponent,
571
template: HydratableComponent.template
572
}
573
};
574
575
return registry[tagName] || null;
576
}
577
578
private populateComponentFromSSR(component: any, element: Element): void {
579
// Extract component data from SSR attributes or content
580
const title = element.querySelector('h1')?.textContent;
581
const description = element.querySelector('p')?.textContent;
582
583
if (title) component.title = title;
584
if (description) component.description = description;
585
}
586
}
587
588
// Initialize SSR hydration
589
const ssrApp = new SSRApp();
590
document.addEventListener('DOMContentLoaded', () => {
591
ssrApp.initializeWithHydration().then(() => {
592
console.log('SSR hydration completed');
593
}).catch(error => {
594
console.error('SSR hydration failed:', error);
595
});
596
});
597
```
598
599
### Hydration Error Handling
600
601
Error handling and debugging utilities for hydration processes, helping identify and resolve hydration mismatches.
602
603
```typescript { .api }
604
/**
605
* Error thrown when hydration fails due to content mismatch
606
*/
607
class HydrationBindingError extends Error {
608
/**
609
* Creates a hydration binding error
610
* @param message - Error message
611
* @param propertyBag - Additional error information
612
*/
613
constructor(
614
message: string | undefined,
615
public readonly propertyBag: {
616
index: number;
617
hydrationStage: string;
618
itemsLength?: number;
619
viewsState: string[];
620
viewTemplateString?: string;
621
rootNodeContent: string;
622
}
623
);
624
}
625
626
/**
627
* Hydration validation utilities
628
*/
629
const HydrationValidator: {
630
/**
631
* Validates that server and client content match
632
* @param serverContent - Content from server-side rendering
633
* @param clientTemplate - Client-side template
634
* @param data - Data used for rendering
635
* @returns Validation result
636
*/
637
validateContentMatch(
638
serverContent: string,
639
clientTemplate: ViewTemplate,
640
data: any
641
): HydrationValidationResult;
642
643
/**
644
* Checks for common hydration issues
645
* @param element - Element being hydrated
646
* @returns Array of potential issues
647
*/
648
checkForHydrationIssues(element: Element): HydrationIssue[];
649
650
/**
651
* Enables debug mode for hydration
652
* @param enabled - Whether to enable debug mode
653
*/
654
setDebugMode(enabled: boolean): void;
655
};
656
657
/**
658
* Result of hydration validation
659
*/
660
interface HydrationValidationResult {
661
/** Whether validation passed */
662
isValid: boolean;
663
664
/** List of validation errors */
665
errors: HydrationValidationError[];
666
667
/** List of validation warnings */
668
warnings: HydrationValidationWarning[];
669
}
670
671
/**
672
* Hydration validation error
673
*/
674
interface HydrationValidationError {
675
/** Error type */
676
type: 'content-mismatch' | 'structure-mismatch' | 'missing-marker';
677
678
/** Error message */
679
message: string;
680
681
/** Expected content */
682
expected: string;
683
684
/** Actual content */
685
actual: string;
686
687
/** Location of error */
688
location: {
689
bindingIndex?: number;
690
elementPath?: string;
691
};
692
}
693
694
/**
695
* Hydration validation warning
696
*/
697
interface HydrationValidationWarning {
698
/** Warning type */
699
type: 'performance' | 'accessibility' | 'seo';
700
701
/** Warning message */
702
message: string;
703
704
/** Suggested fix */
705
suggestion?: string;
706
}
707
708
/**
709
* Potential hydration issue
710
*/
711
interface HydrationIssue {
712
/** Issue severity */
713
severity: 'error' | 'warning' | 'info';
714
715
/** Issue category */
716
category: 'content' | 'structure' | 'performance' | 'accessibility';
717
718
/** Issue description */
719
description: string;
720
721
/** Element causing the issue */
722
element: Element;
723
724
/** Suggested resolution */
725
resolution?: string;
726
}
727
```
728
729
**Usage Examples:**
730
731
```typescript
732
import {
733
HydrationBindingError,
734
HydrationValidator,
735
HydrationValidationResult
736
} from "@microsoft/fast-element";
737
738
// Hydration error handler
739
class HydrationErrorHandler {
740
static handleHydrationError(error: HydrationBindingError): void {
741
console.group('Hydration Error Details');
742
console.error('Message:', error.message);
743
console.log('Binding Index:', error.propertyBag.index);
744
console.log('Hydration Stage:', error.propertyBag.hydrationStage);
745
console.log('Views State:', error.propertyBag.viewsState);
746
console.log('Root Node Content:', error.propertyBag.rootNodeContent);
747
748
if (error.propertyBag.viewTemplateString) {
749
console.log('Template:', error.propertyBag.viewTemplateString);
750
}
751
752
console.groupEnd();
753
754
// Attempt recovery
755
this.attemptHydrationRecovery(error);
756
}
757
758
private static attemptHydrationRecovery(error: HydrationBindingError): void {
759
// Recovery strategies based on error stage
760
switch (error.propertyBag.hydrationStage) {
761
case 'content-binding':
762
this.recoverContentBinding(error);
763
break;
764
case 'attribute-binding':
765
this.recoverAttributeBinding(error);
766
break;
767
case 'event-binding':
768
this.recoverEventBinding(error);
769
break;
770
default:
771
console.warn('No recovery strategy available for stage:', error.propertyBag.hydrationStage);
772
}
773
}
774
775
private static recoverContentBinding(error: HydrationBindingError): void {
776
console.log('Attempting content binding recovery...');
777
// Implement content binding recovery logic
778
}
779
780
private static recoverAttributeBinding(error: HydrationBindingError): void {
781
console.log('Attempting attribute binding recovery...');
782
// Implement attribute binding recovery logic
783
}
784
785
private static recoverEventBinding(error: HydrationBindingError): void {
786
console.log('Attempting event binding recovery...');
787
// Implement event binding recovery logic
788
}
789
}
790
791
// Hydration debugging utility
792
class HydrationDebugger {
793
private static debugMode = false;
794
795
static enableDebugMode(): void {
796
this.debugMode = true;
797
HydrationValidator.setDebugMode(true);
798
console.log('Hydration debug mode enabled');
799
}
800
801
static disableDebugMode(): void {
802
this.debugMode = false;
803
HydrationValidator.setDebugMode(false);
804
console.log('Hydration debug mode disabled');
805
}
806
807
static validateAndLog(
808
serverContent: string,
809
clientTemplate: ViewTemplate,
810
data: any,
811
elementName: string
812
): boolean {
813
if (!this.debugMode) return true;
814
815
console.group(`Hydration Validation: ${elementName}`);
816
817
try {
818
const result = HydrationValidator.validateContentMatch(
819
serverContent,
820
clientTemplate,
821
data
822
);
823
824
this.logValidationResult(result);
825
826
return result.isValid;
827
} catch (error) {
828
console.error('Validation failed:', error);
829
return false;
830
} finally {
831
console.groupEnd();
832
}
833
}
834
835
private static logValidationResult(result: HydrationValidationResult): void {
836
if (result.isValid) {
837
console.log('✅ Hydration validation passed');
838
} else {
839
console.log('❌ Hydration validation failed');
840
}
841
842
if (result.errors.length > 0) {
843
console.group('Errors:');
844
result.errors.forEach((error, index) => {
845
console.error(`${index + 1}. ${error.type}: ${error.message}`);
846
console.log(' Expected:', error.expected);
847
console.log(' Actual:', error.actual);
848
849
if (error.location.bindingIndex !== undefined) {
850
console.log(' Binding Index:', error.location.bindingIndex);
851
}
852
853
if (error.location.elementPath) {
854
console.log(' Element Path:', error.location.elementPath);
855
}
856
});
857
console.groupEnd();
858
}
859
860
if (result.warnings.length > 0) {
861
console.group('Warnings:');
862
result.warnings.forEach((warning, index) => {
863
console.warn(`${index + 1}. ${warning.type}: ${warning.message}`);
864
865
if (warning.suggestion) {
866
console.log(' Suggestion:', warning.suggestion);
867
}
868
});
869
console.groupEnd();
870
}
871
}
872
873
static checkElementForIssues(element: Element): void {
874
if (!this.debugMode) return;
875
876
const issues = HydrationValidator.checkForHydrationIssues(element);
877
878
if (issues.length === 0) {
879
console.log('✅ No hydration issues found for element:', element.tagName);
880
return;
881
}
882
883
console.group(`Hydration Issues for ${element.tagName}:`);
884
885
issues.forEach((issue, index) => {
886
const icon = issue.severity === 'error' ? '❌' :
887
issue.severity === 'warning' ? '⚠️' : 'ℹ️';
888
889
console.log(`${icon} ${index + 1}. [${issue.category}] ${issue.description}`);
890
891
if (issue.resolution) {
892
console.log(` 💡 Resolution: ${issue.resolution}`);
893
}
894
});
895
896
console.groupEnd();
897
}
898
}
899
900
// Production hydration with error handling
901
class ProductionHydrationManager {
902
private errorReporter?: (error: any) => void;
903
904
constructor(errorReporter?: (error: any) => void) {
905
this.errorReporter = errorReporter;
906
}
907
908
async safeHydration<T>(
909
element: Element,
910
template: HydratableElementViewTemplate<T>,
911
data: T
912
): Promise<boolean> {
913
try {
914
// Validate before hydration in development
915
if (process.env.NODE_ENV === 'development') {
916
HydrationDebugger.enableDebugMode();
917
HydrationDebugger.checkElementForIssues(element);
918
}
919
920
// Perform hydration
921
const view = template.hydrate(
922
element.firstChild!,
923
element.lastChild!,
924
element
925
);
926
927
view.bind(data);
928
929
return true;
930
931
} catch (error) {
932
// Handle hydration errors
933
if (error instanceof HydrationBindingError) {
934
HydrationErrorHandler.handleHydrationError(error);
935
} else {
936
console.error('Unexpected hydration error:', error);
937
}
938
939
// Report error in production
940
this.errorReporter?.(error);
941
942
// Fallback to client-side rendering
943
return this.fallbackToCSR(element, template, data);
944
}
945
}
946
947
private async fallbackToCSR<T>(
948
element: Element,
949
template: HydratableElementViewTemplate<T>,
950
data: T
951
): Promise<boolean> {
952
try {
953
console.log('Falling back to client-side rendering');
954
955
// Clear server-rendered content
956
element.innerHTML = '';
957
958
// Create fresh client-side view
959
const view = template.create(element);
960
view.bind(data);
961
view.appendTo(element);
962
963
return true;
964
965
} catch (error) {
966
console.error('Client-side rendering fallback failed:', error);
967
this.errorReporter?.(error);
968
969
return false;
970
}
971
}
972
}
973
974
// Application-level hydration setup
975
class AppHydration {
976
private productionManager = new ProductionHydrationManager(
977
(error) => {
978
// Report to error tracking service
979
console.error('Hydration error reported:', error);
980
}
981
);
982
983
async hydrateApplication(): Promise<void> {
984
const hydratableElements = document.querySelectorAll('[data-hydratable]');
985
986
const hydrationPromises = Array.from(hydratableElements).map(element =>
987
this.hydrateElement(element)
988
);
989
990
const results = await Promise.allSettled(hydrationPromises);
991
992
// Log overall hydration results
993
const successful = results.filter(r => r.status === 'fulfilled').length;
994
const failed = results.filter(r => r.status === 'rejected').length;
995
996
console.log(`Hydration complete: ${successful} successful, ${failed} failed`);
997
998
if (failed > 0) {
999
console.warn('Some components failed to hydrate and fell back to CSR');
1000
}
1001
}
1002
1003
private async hydrateElement(element: Element): Promise<void> {
1004
const componentType = element.getAttribute('data-component-type');
1005
const componentData = this.extractComponentData(element);
1006
1007
// Get component template (would be from registry in real app)
1008
const template = this.getComponentTemplate(componentType!);
1009
1010
if (template && isHydratable(template)) {
1011
const success = await this.productionManager.safeHydration(
1012
element,
1013
template,
1014
componentData
1015
);
1016
1017
if (!success) {
1018
throw new Error(`Failed to hydrate ${componentType}`);
1019
}
1020
}
1021
}
1022
1023
private extractComponentData(element: Element): any {
1024
// Extract component data from element attributes or content
1025
const dataScript = element.querySelector('script[type="application/json"]');
1026
1027
if (dataScript) {
1028
return JSON.parse(dataScript.textContent || '{}');
1029
}
1030
1031
return {};
1032
}
1033
1034
private getComponentTemplate(componentType: string): HydratableElementViewTemplate<any> | null {
1035
// Component template registry
1036
const templates: Record<string, any> = {
1037
'user-card': createHydratableTemplate(),
1038
'product-listing': createHydratableTemplate(),
1039
// Add more component templates
1040
};
1041
1042
return templates[componentType] || null;
1043
}
1044
}
1045
```
1046
1047
## Types
1048
1049
```typescript { .api }
1050
/**
1051
* Hydration stage type
1052
*/
1053
type HydrationStage =
1054
| "initial"
1055
| "content-binding"
1056
| "attribute-binding"
1057
| "event-binding"
1058
| "complete";
1059
1060
/**
1061
* Hydration marker type
1062
*/
1063
interface HydrationMarker {
1064
/** Marker type */
1065
type: 'content' | 'attribute' | 'element';
1066
1067
/** Binding index */
1068
index: number;
1069
1070
/** Target node ID */
1071
targetNodeId?: string;
1072
1073
/** Attribute name for attribute markers */
1074
attributeName?: string;
1075
}
1076
1077
/**
1078
* Hydration context interface
1079
*/
1080
interface HydrationContext {
1081
/** Current hydration stage */
1082
stage: HydrationStage;
1083
1084
/** Available hydration markers */
1085
markers: HydrationMarker[];
1086
1087
/** Root element being hydrated */
1088
rootElement: Element;
1089
1090
/** Component data */
1091
data: any;
1092
}
1093
```