Skip to content

Commit 9f490da

Browse files
AndrewKushnirdylhunn
authored andcommitted
fix(core): handle hydration of view containers for root components (#51247)
For cases when a root component also acts as an anchor node for a ViewContainerRef (for example, when ViewContainerRef is injected in a root component), there is a need to serialize information about the component itself, as well as an LContainer that represents this ViewContainerRef. Effectively, we need to serialize 2 pieces of info: (1) hydration info for the root component itself and (2) hydration info for the ViewContainerRef instance (an LContainer). Each piece of information is included into the hydration data (in the TransferState object) separately, thus we end up with 2 ids. Since we only have 1 root element, we encode both bits of info into a single string: ids are separated by the `|` char (e.g. `10|25`, where `10` is the ngh for a component view and 25 is the `ngh` for a root view which holds LContainer). Previously, we were only including component-related information, thus all the views in the view container remained dehydrated and duplicated (client-rendered from scratch) on the client. Resolves #51157. PR Close #51247
1 parent b95b5b5 commit 9f490da

6 files changed

Lines changed: 458 additions & 54 deletions

File tree

packages/core/src/hydration/annotate.ts

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,22 @@
88

99
import {ApplicationRef} from '../application_ref';
1010
import {ViewEncapsulation} from '../metadata';
11+
import {Renderer2} from '../render';
1112
import {collectNativeNodes} from '../render3/collect_native_nodes';
1213
import {getComponentDef} from '../render3/definition';
1314
import {CONTAINER_HEADER_OFFSET, LContainer} from '../render3/interfaces/container';
1415
import {TNode, TNodeType} from '../render3/interfaces/node';
1516
import {RElement} from '../render3/interfaces/renderer_dom';
1617
import {hasI18n, isComponentHost, isLContainer, isProjectionTNode, isRootView} from '../render3/interfaces/type_checks';
17-
import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, RENDERER, TView, TVIEW, TViewType} from '../render3/interfaces/view';
18+
import {CONTEXT, HEADER_OFFSET, HOST, LView, PARENT, RENDERER, TView, TVIEW, TViewType} from '../render3/interfaces/view';
1819
import {unwrapRNode} from '../render3/util/view_utils';
1920
import {TransferState} from '../transfer_state';
2021

2122
import {unsupportedProjectionOfDomNodes} from './error_handling';
2223
import {CONTAINERS, DISCONNECTED_NODES, ELEMENT_CONTAINERS, MULTIPLIER, NODES, NUM_ROOT_NODES, SerializedContainerView, SerializedView, TEMPLATE_ID, TEMPLATES} from './interfaces';
2324
import {calcPathForNode} from './node_lookup_utils';
24-
import {hasInSkipHydrationBlockFlag, isInSkipHydrationBlock, SKIP_HYDRATION_ATTR_NAME} from './skip_hydration';
25-
import {getComponentLViewForHydration, NGH_ATTR_NAME, NGH_DATA_KEY, TextNodeMarker} from './utils';
25+
import {isInSkipHydrationBlock, SKIP_HYDRATION_ATTR_NAME} from './skip_hydration';
26+
import {getLNodeForHydration, NGH_ATTR_NAME, NGH_DATA_KEY, TextNodeMarker} from './utils';
2627

2728
/**
2829
* A collection that tracks all serialized views (`ngh` DOM annotations)
@@ -91,6 +92,54 @@ function calcNumRootNodes(tView: TView, lView: LView, tNode: TNode|null): number
9192
return rootNodes.length;
9293
}
9394

95+
/**
96+
* Annotates root level component's LView for hydration,
97+
* see `annotateHostElementForHydration` for additional information.
98+
*/
99+
function annotateComponentLViewForHydration(lView: LView, context: HydrationContext): number|null {
100+
const hostElement = lView[HOST];
101+
// Root elements might also be annotated with the `ngSkipHydration` attribute,
102+
// check if it's present before starting the serialization process.
103+
if (hostElement && !(hostElement as HTMLElement).hasAttribute(SKIP_HYDRATION_ATTR_NAME)) {
104+
return annotateHostElementForHydration(hostElement as HTMLElement, lView, context);
105+
}
106+
return null;
107+
}
108+
109+
/**
110+
* Annotates root level LContainer for hydration. This happens when a root component
111+
* injects ViewContainerRef, thus making the component an anchor for a view container.
112+
* This function serializes the component itself as well as all views from the view
113+
* container.
114+
*/
115+
function annotateLContainerForHydration(lContainer: LContainer, context: HydrationContext) {
116+
const componentLView = lContainer[HOST] as LView<unknown>;
117+
118+
// Serialize the root component itself.
119+
const componentLViewNghIndex = annotateComponentLViewForHydration(componentLView, context);
120+
121+
const hostElement = unwrapRNode(componentLView[HOST]!) as HTMLElement;
122+
123+
// Serialize all views within this view container.
124+
const rootLView = lContainer[PARENT];
125+
const rootLViewNghIndex = annotateHostElementForHydration(hostElement, rootLView, context);
126+
127+
const renderer = componentLView[RENDERER] as Renderer2;
128+
129+
// For cases when a root component also acts as an anchor node for a ViewContainerRef
130+
// (for example, when ViewContainerRef is injected in a root component), there is a need
131+
// to serialize information about the component itself, as well as an LContainer that
132+
// represents this ViewContainerRef. Effectively, we need to serialize 2 pieces of info:
133+
// (1) hydration info for the root component itself and (2) hydration info for the
134+
// ViewContainerRef instance (an LContainer). Each piece of information is included into
135+
// the hydration data (in the TransferState object) separately, thus we end up with 2 ids.
136+
// Since we only have 1 root element, we encode both bits of info into a single string:
137+
// ids are separated by the `|` char (e.g. `10|25`, where `10` is the ngh for a component view
138+
// and 25 is the `ngh` for a root view which holds LContainer).
139+
const finalIndex = `${componentLViewNghIndex}|${rootLViewNghIndex}`;
140+
renderer.setAttribute(hostElement, NGH_ATTR_NAME, finalIndex);
141+
}
142+
94143
/**
95144
* Annotates all components bootstrapped in a given ApplicationRef
96145
* with info needed for hydration.
@@ -103,21 +152,21 @@ export function annotateForHydration(appRef: ApplicationRef, doc: Document) {
103152
const corruptedTextNodes = new Map<HTMLElement, TextNodeMarker>();
104153
const viewRefs = appRef._views;
105154
for (const viewRef of viewRefs) {
106-
const lView = getComponentLViewForHydration(viewRef);
155+
const lNode = getLNodeForHydration(viewRef);
156+
107157
// An `lView` might be `null` if a `ViewRef` represents
108158
// an embedded view (not a component view).
109-
if (lView !== null) {
110-
const hostElement = lView[HOST];
111-
// Root elements might also be annotated with the `ngSkipHydration` attribute,
112-
// check if it's present before starting the serialization process.
113-
if (hostElement && !(hostElement as HTMLElement).hasAttribute(SKIP_HYDRATION_ATTR_NAME)) {
114-
const context: HydrationContext = {
115-
serializedViewCollection,
116-
corruptedTextNodes,
117-
};
118-
annotateHostElementForHydration(hostElement as HTMLElement, lView, context);
119-
insertCorruptedTextNodeMarkers(corruptedTextNodes, doc);
159+
if (lNode !== null) {
160+
const context: HydrationContext = {
161+
serializedViewCollection,
162+
corruptedTextNodes,
163+
};
164+
if (isLContainer(lNode)) {
165+
annotateLContainerForHydration(lNode, context);
166+
} else {
167+
annotateComponentLViewForHydration(lNode, context);
120168
}
169+
insertCorruptedTextNodeMarkers(corruptedTextNodes, doc);
121170
}
122171
}
123172

@@ -408,9 +457,11 @@ function componentUsesShadowDomEncapsulation(lView: LView): boolean {
408457
* @param element The Host element to be annotated
409458
* @param lView The associated LView
410459
* @param context The hydration context
460+
* @returns An index of serialized view from the transfer state object
461+
* or `null` when a given component can not be serialized.
411462
*/
412463
function annotateHostElementForHydration(
413-
element: RElement, lView: LView, context: HydrationContext): void {
464+
element: RElement, lView: LView, context: HydrationContext): number|null {
414465
const renderer = lView[RENDERER];
415466
if (hasI18n(lView) || componentUsesShadowDomEncapsulation(lView)) {
416467
// Attach the skip hydration attribute if this component:
@@ -419,10 +470,12 @@ function annotateHostElementForHydration(
419470
// shadow DOM, so we can not guarantee that client and server representations
420471
// would exactly match
421472
renderer.setAttribute(element, SKIP_HYDRATION_ATTR_NAME, '');
473+
return null;
422474
} else {
423475
const ngh = serializeLView(lView, context);
424476
const index = context.serializedViewCollection.add(ngh);
425477
renderer.setAttribute(element, NGH_ATTR_NAME, index.toString());
478+
return index;
426479
}
427480
}
428481

packages/core/src/hydration/cleanup.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import {ApplicationRef} from '../application_ref';
1010
import {CONTAINER_HEADER_OFFSET, DEHYDRATED_VIEWS, LContainer} from '../render3/interfaces/container';
1111
import {Renderer} from '../render3/interfaces/renderer';
1212
import {RNode} from '../render3/interfaces/renderer_dom';
13-
import {isLContainer} from '../render3/interfaces/type_checks';
13+
import {isLContainer, isLView} from '../render3/interfaces/type_checks';
1414
import {HEADER_OFFSET, HOST, LView, PARENT, RENDERER, TVIEW} from '../render3/interfaces/view';
1515
import {nativeRemoveNode} from '../render3/node_manipulation';
1616
import {EMPTY_ARRAY} from '../util/empty';
1717

1818
import {validateSiblingNodeExists} from './error_handling';
1919
import {DehydratedContainerView, NUM_ROOT_NODES} from './interfaces';
20-
import {getComponentLViewForHydration} from './utils';
20+
import {getLNodeForHydration} from './utils';
2121

2222
/**
2323
* Removes all dehydrated views from a given LContainer:
@@ -92,11 +92,20 @@ function cleanupLView(lView: LView) {
9292
export function cleanupDehydratedViews(appRef: ApplicationRef) {
9393
const viewRefs = appRef._views;
9494
for (const viewRef of viewRefs) {
95-
const lView = getComponentLViewForHydration(viewRef);
95+
const lNode = getLNodeForHydration(viewRef);
9696
// An `lView` might be `null` if a `ViewRef` represents
9797
// an embedded view (not a component view).
98-
if (lView !== null && lView[HOST] !== null) {
99-
cleanupLView(lView);
98+
if (lNode !== null && lNode[HOST] !== null) {
99+
if (isLView(lNode)) {
100+
cleanupLView(lNode);
101+
} else {
102+
// Cleanup in the root component view
103+
const componentLView = lNode[HOST] as LView<unknown>;
104+
cleanupLView(componentLView);
105+
106+
// Cleanup in all views within this view container
107+
cleanupLContainer(lNode);
108+
}
100109
ngDevMode && ngDevMode.dehydratedViewsCleanupRuns++;
101110
}
102111
}

packages/core/src/hydration/utils.ts

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99

1010
import {Injector} from '../di/injector';
1111
import {ViewRef} from '../linker/view_ref';
12+
import {LContainer} from '../render3/interfaces/container';
1213
import {getDocument} from '../render3/interfaces/document';
1314
import {RElement, RNode} from '../render3/interfaces/renderer_dom';
14-
import {isLContainer, isRootView} from '../render3/interfaces/type_checks';
15-
import {HEADER_OFFSET, HOST, LView, TVIEW, TViewType} from '../render3/interfaces/view';
15+
import {isRootView} from '../render3/interfaces/type_checks';
16+
import {HEADER_OFFSET, LView, TVIEW, TViewType} from '../render3/interfaces/view';
1617
import {makeStateKey, TransferState} from '../transfer_state';
1718
import {assertDefined} from '../util/assert';
1819

@@ -69,15 +70,34 @@ export const enum TextNodeMarker {
6970
*
7071
* @param rNode Component's host element.
7172
* @param injector Injector that this component has access to.
73+
* @param isRootView Specifies whether we trying to read hydration info for the root view.
7274
*/
7375
let _retrieveHydrationInfoImpl: typeof retrieveHydrationInfoImpl =
74-
(rNode: RElement, injector: Injector) => null;
76+
(rNode: RElement, injector: Injector, isRootView?: boolean) => null;
7577

76-
export function retrieveHydrationInfoImpl(rNode: RElement, injector: Injector): DehydratedView|
77-
null {
78-
const nghAttrValue = rNode.getAttribute(NGH_ATTR_NAME);
78+
export function retrieveHydrationInfoImpl(
79+
rNode: RElement, injector: Injector, isRootView = false): DehydratedView|null {
80+
let nghAttrValue = rNode.getAttribute(NGH_ATTR_NAME);
7981
if (nghAttrValue == null) return null;
8082

83+
// For cases when a root component also acts as an anchor node for a ViewContainerRef
84+
// (for example, when ViewContainerRef is injected in a root component), there is a need
85+
// to serialize information about the component itself, as well as an LContainer that
86+
// represents this ViewContainerRef. Effectively, we need to serialize 2 pieces of info:
87+
// (1) hydration info for the root component itself and (2) hydration info for the
88+
// ViewContainerRef instance (an LContainer). Each piece of information is included into
89+
// the hydration data (in the TransferState object) separately, thus we end up with 2 ids.
90+
// Since we only have 1 root element, we encode both bits of info into a single string:
91+
// ids are separated by the `|` char (e.g. `10|25`, where `10` is the ngh for a component view
92+
// and 25 is the `ngh` for a root view which holds LContainer).
93+
const [componentViewNgh, rootViewNgh] = nghAttrValue.split('|');
94+
nghAttrValue = isRootView ? rootViewNgh : componentViewNgh;
95+
if (!nghAttrValue) return null;
96+
97+
// We've read one of the ngh ids, keep the remaining one, so that
98+
// we can set it back on the DOM element.
99+
const remainingNgh = isRootView ? componentViewNgh : (rootViewNgh ? `|${rootViewNgh}` : '');
100+
81101
let data: SerializedView = {};
82102
// An element might have an empty `ngh` attribute value (e.g. `<comp ngh="" />`),
83103
// which means that no special annotations are required. Do not attempt to read
@@ -101,9 +121,31 @@ export function retrieveHydrationInfoImpl(rNode: RElement, injector: Injector):
101121
data,
102122
firstChild: rNode.firstChild ?? null,
103123
};
104-
// The `ngh` attribute is cleared from the DOM node now
105-
// that the data has been retrieved.
106-
rNode.removeAttribute(NGH_ATTR_NAME);
124+
125+
if (isRootView) {
126+
// If there is hydration info present for the root view, it means that there was
127+
// a ViewContainerRef injected in the root component. The root component host element
128+
// acted as an anchor node in this scenario. As a result, the DOM nodes that represent
129+
// embedded views in this ViewContainerRef are located as siblings to the host node,
130+
// i.e. `<app-root /><#VIEW1><#VIEW2>...<!--container-->`. In this case, the current
131+
// node becomes the first child of this root view and the next sibling is the first
132+
// element in the DOM segment.
133+
dehydratedView.firstChild = rNode;
134+
135+
// We use `0` here, since this is the slot (right after the HEADER_OFFSET)
136+
// where a component LView or an LContainer is located in a root LView.
137+
setSegmentHead(dehydratedView, 0, rNode.nextSibling);
138+
}
139+
140+
if (remainingNgh) {
141+
// If we have only used one of the ngh ids, store the remaining one
142+
// back on this RNode.
143+
rNode.setAttribute(NGH_ATTR_NAME, remainingNgh);
144+
} else {
145+
// The `ngh` attribute is cleared from the DOM node now
146+
// that the data has been retrieved for all indices.
147+
rNode.removeAttribute(NGH_ATTR_NAME);
148+
}
107149

108150
// Note: don't check whether this node was claimed for hydration,
109151
// because this node might've been previously claimed while processing
@@ -125,15 +167,18 @@ export function enableRetrieveHydrationInfoImpl() {
125167
* Retrieves hydration info by reading the value from the `ngh` attribute
126168
* and accessing a corresponding slot in TransferState storage.
127169
*/
128-
export function retrieveHydrationInfo(rNode: RElement, injector: Injector): DehydratedView|null {
129-
return _retrieveHydrationInfoImpl(rNode, injector);
170+
export function retrieveHydrationInfo(
171+
rNode: RElement, injector: Injector, isRootView = false): DehydratedView|null {
172+
return _retrieveHydrationInfoImpl(rNode, injector, isRootView);
130173
}
131174

132175
/**
133-
* Retrieves an instance of a component LView from a given ViewRef.
134-
* Returns an instance of a component LView or `null` in case of an embedded view.
176+
* Retrieves the necessary object from a given ViewRef to serialize:
177+
* - an LView for component views
178+
* - an LContainer for cases when component acts as a ViewContainerRef anchor
179+
* - `null` in case of an embedded view
135180
*/
136-
export function getComponentLViewForHydration(viewRef: ViewRef): LView|null {
181+
export function getLNodeForHydration(viewRef: ViewRef): LView|LContainer|null {
137182
// Reading an internal field from `ViewRef` instance.
138183
let lView = (viewRef as any)._lView as LView;
139184
const tView = lView[TVIEW];
@@ -148,12 +193,6 @@ export function getComponentLViewForHydration(viewRef: ViewRef): LView|null {
148193
lView = lView[HEADER_OFFSET];
149194
}
150195

151-
// If a `ViewContainerRef` was injected in a component class, this resulted
152-
// in an LContainer creation at that location. In this case, the component
153-
// LView is in the LContainer's `HOST` slot.
154-
if (isLContainer(lView)) {
155-
lView = lView[HOST];
156-
}
157196
return lView;
158197
}
159198

packages/core/src/render3/component_ref.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,12 +215,17 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
215215
LViewFlags.CheckAlways | LViewFlags.IsRoot;
216216
const rootFlags = this.componentDef.signals ? signalFlags : nonSignalFlags;
217217

218+
let hydrationInfo: DehydratedView|null = null;
219+
if (hostRNode !== null) {
220+
hydrationInfo = retrieveHydrationInfo(hostRNode, rootViewInjector, true /* isRootView */);
221+
}
222+
218223
// Create the root view. Uses empty TView and ContentTemplate.
219224
const rootTView =
220225
createTView(TViewType.Root, null, null, 1, 0, null, null, null, null, null, null);
221226
const rootLView = createLView(
222227
null, rootTView, null, rootFlags, null, null, environment, hostRenderer, rootViewInjector,
223-
null, null);
228+
null, hydrationInfo);
224229

225230
// rootView is the parent when bootstrapping
226231
// TODO(misko): it looks like we are entering view here but we don't really need to as
@@ -382,7 +387,7 @@ function createRootComponentView(
382387
const tView = rootView[TVIEW];
383388
applyRootComponentStyling(rootDirectives, tNode, hostRNode, hostRenderer);
384389

385-
// Hydration info is on the host element and needs to be retreived
390+
// Hydration info is on the host element and needs to be retrieved
386391
// and passed to the component LView.
387392
let hydrationInfo: DehydratedView|null = null;
388393
if (hostRNode !== null) {

packages/core/test/bundling/hydration/bundle.golden_symbols.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -788,9 +788,6 @@
788788
{
789789
"name": "getComponentLViewByIndex"
790790
},
791-
{
792-
"name": "getComponentLViewForHydration"
793-
},
794791
{
795792
"name": "getCurrentTNode"
796793
},
@@ -827,6 +824,9 @@
827824
{
828825
"name": "getInjectorIndex"
829826
},
827+
{
828+
"name": "getLNodeForHydration"
829+
},
830830
{
831831
"name": "getLView"
832832
},
@@ -1235,6 +1235,9 @@
12351235
{
12361236
"name": "setInjectImplementation"
12371237
},
1238+
{
1239+
"name": "setSegmentHead"
1240+
},
12381241
{
12391242
"name": "setSelectedIndex"
12401243
},

0 commit comments

Comments
 (0)