Skip to content

Commit 761e02d

Browse files
AndrewKushnirdylhunn
authored andcommitted
feat(platform-browser): add a public API function to enable non-destructive hydration (#49666)
This commit adds the `provideClientHydration` function to the public API. This function can be used to enable the non-destructive Angular hydration. Important note: the non-destructive hydration feature is in Developer Preview mode, learn more about it at https://angular.io/guide/releases#developer-preview. Before you can get started with hydration, you must have a server side rendered (SSR) application. Follow the [Angular Universal Guide](https://angular.io/guide/universal) to enable server side rendering first. Once you have SSR working with your application, you can enable hydration by visiting your main app component or module and importing `provideClientHydration` from `@angular/platform-browser`. You'll then add that provider to your app's bootstrapping providers list. ```typescript import { bootstrapApplication, provideClientHydration, } from '@angular/platform-browser'; // ... bootstrapApplication(RootCmp, { providers: [provideClientHydration()] }); ``` Alternatively if you are using NgModules, you would add `provideClientHydration` to your root app module's provider list. ```typescript import {provideClientHydration} from '@angular/platform-browser'; import {NgModule} from '@angular/core'; @NgModule({ declarations: [RootCmp], exports: [RootCmp], bootstrap: [RootCmp], providers: [provideClientHydration()], }) export class AppModule {} ``` You can confirm hydration is enabled by opening Developer Tools in your browser and viewing the console. You should see a message that includes hydration-related stats, such as the number of components and nodes hydrated. Co-authored-by: jessicajaniuk <[email protected]> Co-authored-by: alan-agius4 <[email protected]> PR Close #49666
1 parent b2327f4 commit 761e02d

File tree

6 files changed

+224
-44
lines changed

6 files changed

+224
-44
lines changed

goldens/public-api/platform-browser/index.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ApplicationRef } from '@angular/core';
99
import { ComponentRef } from '@angular/core';
1010
import { DebugElement } from '@angular/core';
1111
import { DebugNode } from '@angular/core';
12+
import { EnvironmentProviders } from '@angular/core';
1213
import * as i0 from '@angular/core';
1314
import * as i1 from '@angular/common';
1415
import { InjectionToken } from '@angular/core';
@@ -143,6 +144,17 @@ export class HammerModule {
143144
static ɵmod: i0.ɵɵNgModuleDeclaration<HammerModule, never, never, never>;
144145
}
145146

147+
// @public
148+
export interface HydrationFeature<FeatureKind extends HydrationFeatureKind> {
149+
// (undocumented)
150+
ɵkind: FeatureKind;
151+
// (undocumented)
152+
ɵproviders: Provider[];
153+
}
154+
155+
// @public
156+
export type HydrationFeatures = NoDomReuseFeature;
157+
146158
// @public @deprecated
147159
export const makeStateKey: typeof makeStateKey_2;
148160

@@ -177,9 +189,15 @@ export type MetaDefinition = {
177189
[prop: string]: string;
178190
};
179191

192+
// @public
193+
export type NoDomReuseFeature = HydrationFeature<HydrationFeatureKind.NoDomReuseFeature>;
194+
180195
// @public
181196
export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef;
182197

198+
// @public
199+
export function provideClientHydration(...features: HydrationFeatures[]): EnvironmentProviders;
200+
183201
// @public
184202
export function provideProtractorTestingSupport(): Provider[];
185203

@@ -232,6 +250,9 @@ export const TransferState: {
232250
// @public (undocumented)
233251
export const VERSION: Version;
234252

253+
// @public
254+
export function withoutDomReuse(): NoDomReuseFeature;
255+
235256
// (No @packageDocumentation comment for this package)
236257

237258
```

packages/core/src/core_private_export.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export {INJECTOR_SCOPE as ɵINJECTOR_SCOPE} from './di/scope';
1616
export {XSS_SECURITY_URL as ɵXSS_SECURITY_URL} from './error_details_base_url';
1717
export {formatRuntimeError as ɵformatRuntimeError, RuntimeError as ɵRuntimeError} from './errors';
1818
export {annotateForHydration as ɵannotateForHydration} from './hydration/annotate';
19-
export {provideHydrationSupport as ɵprovideHydrationSupport} from './hydration/api';
19+
export {withDomHydration as ɵwithDomHydration} from './hydration/api';
2020
export {IS_HYDRATION_FEATURE_ENABLED as ɵIS_HYDRATION_FEATURE_ENABLED} from './hydration/tokens';
2121
export {CurrencyIndex as ɵCurrencyIndex, ExtraLocaleDataIndex as ɵExtraLocaleDataIndex, findLocaleData as ɵfindLocaleData, getLocaleCurrencyCode as ɵgetLocaleCurrencyCode, getLocalePluralCase as ɵgetLocalePluralCase, LocaleDataIndex as ɵLocaleDataIndex, registerLocaleData as ɵregisterLocaleData, unregisterAllLocaleData as ɵunregisterLocaleData} from './i18n/locale_data_api';
2222
export {DEFAULT_LOCALE_ID as ɵDEFAULT_LOCALE_ID} from './i18n/localization';

packages/core/src/hydration/api.ts

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ function printHydrationStats(console: Console) {
7474
const message = `Angular hydrated ${ngDevMode!.hydratedComponents} component(s) ` +
7575
`and ${ngDevMode!.hydratedNodes} node(s), ` +
7676
`${ngDevMode!.componentsSkippedHydration} component(s) were skipped. ` +
77+
`Note: this feature is in Developer Preview mode. ` +
7778
`Learn more at https://angular.io/guides/hydration.`;
7879
// tslint:disable-next-line:no-console
7980
console.log(message);
@@ -92,47 +93,15 @@ function whenStable(
9293

9394
/**
9495
* Returns a set of providers required to setup hydration support
95-
* for an application that is server side rendered.
96-
*
97-
* ## NgModule-based bootstrap
98-
*
99-
* You can add the function call to the root AppModule of an application:
100-
* ```
101-
* import {provideHydrationSupport} from '@angular/core';
102-
*
103-
* @NgModule({
104-
* providers: [
105-
* // ... other providers ...
106-
* provideHydrationSupport()
107-
* ],
108-
* declarations: [AppComponent],
109-
* bootstrap: [AppComponent]
110-
* })
111-
* class AppModule {}
112-
* ```
113-
*
114-
* ## Standalone-based bootstrap
115-
*
116-
* Add the function to the `bootstrapApplication` call:
117-
* ```
118-
* import {provideHydrationSupport} from '@angular/core';
119-
*
120-
* bootstrapApplication(RootComponent, {
121-
* providers: [
122-
* // ... other providers ...
123-
* provideHydrationSupport()
124-
* ]
125-
* });
126-
* ```
96+
* for an application that is server side rendered. This function is
97+
* included into the `provideClientHydration` public API function from
98+
* the `platform-browser` package.
12799
*
128100
* The function sets up an internal flag that would be recognized during
129101
* the server side rendering time as well, so there is no need to
130102
* configure or change anything in NgUniversal to enable the feature.
131-
*
132-
* @publicApi
133-
* @developerPreview
134103
*/
135-
export function provideHydrationSupport(): EnvironmentProviders {
104+
export function withDomHydration(): EnvironmentProviders {
136105
return makeEnvironmentProviders([
137106
{
138107
provide: ENVIRONMENT_INITIALIZER,
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {EnvironmentProviders, makeEnvironmentProviders, Provider, ɵwithDomHydration as withDomHydration} from '@angular/core';
10+
11+
/**
12+
* The list of features as an enum to uniquely type each feature.
13+
*/
14+
export const enum HydrationFeatureKind {
15+
NoDomReuseFeature
16+
}
17+
18+
/**
19+
* Helper type to represent a Hydration feature.
20+
*
21+
* @publicApi
22+
* @developerPreview
23+
*/
24+
export interface HydrationFeature<FeatureKind extends HydrationFeatureKind> {
25+
ɵkind: FeatureKind;
26+
ɵproviders: Provider[];
27+
}
28+
29+
/**
30+
* Helper function to create an object that represents a Hydration feature.
31+
*/
32+
function hydrationFeature<FeatureKind extends HydrationFeatureKind>(
33+
kind: FeatureKind, providers: Provider[]): HydrationFeature<FeatureKind> {
34+
return {ɵkind: kind, ɵproviders: providers};
35+
}
36+
37+
/**
38+
* A type alias that represents a feature which disables DOM reuse during hydration
39+
* (effectively making Angular re-render the whole application from scratch).
40+
* The type is used to describe the return value of the `withoutDomReuse` function.
41+
*
42+
* @see `withoutDomReuse`
43+
* @see `provideClientHydration`
44+
*
45+
* @publicApi
46+
* @developerPreview
47+
*/
48+
export type NoDomReuseFeature = HydrationFeature<HydrationFeatureKind.NoDomReuseFeature>;
49+
50+
/**
51+
* Disables DOM nodes reuse during hydration. Effectively makes
52+
* Angular re-render an application from scratch on the client.
53+
*
54+
* @publicApi
55+
* @developerPreview
56+
*/
57+
export function withoutDomReuse(): NoDomReuseFeature {
58+
// This feature has no providers and acts as a flag that turns off
59+
// non-destructive hydration (which otherwise is turned on by default).
60+
const providers: Provider[] = [];
61+
return hydrationFeature(HydrationFeatureKind.NoDomReuseFeature, providers);
62+
}
63+
64+
/**
65+
* A type alias that represents all Hydration features available for use with
66+
* `provideClientHydration`. Features can be enabled by adding special functions to the
67+
* `provideClientHydration` call. See documentation for each symbol to find corresponding
68+
* function name. See also `provideClientHydration` documentation on how to use those functions.
69+
*
70+
* @see `provideClientHydration`
71+
*
72+
* @publicApi
73+
* @developerPreview
74+
*/
75+
export type HydrationFeatures = NoDomReuseFeature;
76+
77+
/**
78+
* Sets up providers necessary to enable hydration functionality for the application.
79+
* By default, the function enables the recommended set of features for the optimal
80+
* performance for most of the applications. You can enable/disable features by
81+
* passing special functions (from the `HydrationFeatures` set) as arguments to the
82+
* `provideClientHydration` function.
83+
*
84+
* @usageNotes
85+
*
86+
* Basic example of how you can enable hydration in your application when
87+
* `bootstrapApplication` function is used:
88+
* ```
89+
* bootstrapApplication(AppComponent, {
90+
* providers: [provideClientHydration()]
91+
* });
92+
* ```
93+
*
94+
* Alternatively if you are using NgModules, you would add `provideClientHydration`
95+
* to your root app module's provider list.
96+
* ```
97+
* @NgModule({
98+
* declarations: [RootCmp],
99+
* bootstrap: [RootCmp],
100+
* providers: [provideClientHydration()],
101+
* })
102+
* export class AppModule {}
103+
* ```
104+
*
105+
* @see `HydrationFeatures`
106+
*
107+
* @param features Optional features to configure additional router behaviors.
108+
* @returns A set of providers to enable hydration.
109+
*
110+
* @publicApi
111+
* @developerPreview
112+
*/
113+
export function provideClientHydration(...features: HydrationFeatures[]): EnvironmentProviders {
114+
const shouldUseDomHydration =
115+
!features.find(feature => feature.ɵkind === HydrationFeatureKind.NoDomReuseFeature);
116+
return makeEnvironmentProviders([
117+
(shouldUseDomHydration ? withDomHydration() : []),
118+
features.map(feature => feature.ɵproviders),
119+
]);
120+
}

packages/platform-browser/src/platform-browser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export {REMOVE_STYLES_ON_COMPONENT_DESTROY} from './dom/dom_renderer';
7777
export {EVENT_MANAGER_PLUGINS, EventManager} from './dom/events/event_manager';
7878
export {HAMMER_GESTURE_CONFIG, HAMMER_LOADER, HammerGestureConfig, HammerLoader, HammerModule} from './dom/events/hammer_gestures';
7979
export {DomSanitizer, SafeHtml, SafeResourceUrl, SafeScript, SafeStyle, SafeUrl, SafeValue} from './security/dom_sanitization_service';
80+
export {HydrationFeature, provideClientHydration, NoDomReuseFeature, HydrationFeatures, withoutDomReuse} from './hydration';
8081

8182
export * from './private_export';
8283
export {VERSION} from './version';

packages/platform-server/test/hydration_spec.ts

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import '@angular/localize/init';
1010

1111
import {CommonModule, DOCUMENT, isPlatformServer, NgComponentOutlet, NgFor, NgIf, NgTemplateOutlet, PlatformLocation} from '@angular/common';
1212
import {MockPlatformLocation} from '@angular/common/testing';
13-
import {ApplicationRef, Component, ComponentRef, createComponent, destroyPlatform, Directive, ElementRef, EnvironmentInjector, ErrorHandler, getPlatform, inject, Injectable, Input, NgZone, PLATFORM_ID, Provider, TemplateRef, Type, ViewChild, ViewContainerRef, ɵprovideHydrationSupport as provideHydrationSupport, ɵsetDocument} from '@angular/core';
13+
import {ApplicationRef, Component, ComponentRef, createComponent, destroyPlatform, Directive, ElementRef, EnvironmentInjector, ErrorHandler, getPlatform, inject, Injectable, Input, NgZone, PLATFORM_ID, Provider, TemplateRef, Type, ViewChild, ViewContainerRef, ɵsetDocument} from '@angular/core';
1414
import {Console} from '@angular/core/src/console';
1515
import {InitialRenderPendingTasks} from '@angular/core/src/initial_render_pending_tasks';
1616
import {getComponentDef} from '@angular/core/src/render3/definition';
1717
import {unescapeTransferStateContent} from '@angular/core/src/transfer_state';
1818
import {TestBed} from '@angular/core/testing';
19-
import {bootstrapApplication} from '@angular/platform-browser';
19+
import {bootstrapApplication, HydrationFeatures, provideClientHydration, withoutDomReuse} from '@angular/platform-browser';
2020
import {provideRouter, RouterOutlet, Routes} from '@angular/router';
2121
import {first} from 'rxjs/operators';
2222

@@ -128,6 +128,25 @@ function verifyAllNodesClaimedForHydration(el: HTMLElement, exceptions: HTMLElem
128128
}
129129
}
130130

131+
/**
132+
* Walks over DOM nodes starting from a given node and make sure
133+
* those nodes were not annotated as "claimed" by hydration.
134+
* This helper function is needed to verify that the non-destructive
135+
* hydration feature can be turned off.
136+
*/
137+
function verifyNoNodesWereClaimedForHydration(el: HTMLElement) {
138+
if ((el as any).__claimed) {
139+
fail(
140+
'Unexpected state: the following node was hydrated, when the test ' +
141+
'expects the node to be re-created instead: ' + el.outerHTML);
142+
}
143+
let current = el.firstChild;
144+
while (current) {
145+
verifyNoNodesWereClaimedForHydration(current as HTMLElement);
146+
current = current.nextSibling;
147+
}
148+
}
149+
131150
/**
132151
* Verifies whether a console has a log entry that contains a given message.
133152
*/
@@ -138,6 +157,17 @@ function verifyHasLog(appRef: ApplicationRef, message: string) {
138157
expect(console.logs.some(log => log.includes(message))).withContext(context).toBe(true);
139158
}
140159

160+
/**
161+
* Verifies that there is no message with a particular content in a console.
162+
*/
163+
function verifyHasNoLog(appRef: ApplicationRef, message: string) {
164+
const console = appRef.injector.get(Console) as DebugConsole;
165+
const context = `Expected '${message}' to be present in the log, but it was not found. ` +
166+
`Logs content: ${JSON.stringify(console.logs)}`;
167+
expect(console.logs.some(log => log.includes(message))).withContext(context).toBe(false);
168+
}
169+
170+
141171
/**
142172
* Reset TView, so that we re-enter the first create pass as
143173
* we would normally do when we hydrate on the client. Otherwise,
@@ -215,12 +245,13 @@ describe('platform-server integration', () => {
215245
* @returns a promise containing the server rendered app as a string
216246
*/
217247
async function ssr(
218-
component: Type<unknown>, doc?: string, envProviders?: Provider[]): Promise<string> {
248+
component: Type<unknown>, doc?: string, envProviders?: Provider[],
249+
hydrationFeatures?: HydrationFeatures[]): Promise<string> {
219250
const defaultHtml = '<html><head></head><body><app></app></body></html>';
220251
const providers = [
221252
...(envProviders ?? []),
222253
provideServerRendering(),
223-
provideHydrationSupport(),
254+
provideClientHydration(...(hydrationFeatures || [])),
224255
];
225256

226257
const bootstrap = () => bootstrapApplication(component, {providers});
@@ -239,8 +270,9 @@ describe('platform-server integration', () => {
239270
* @param envProviders the environment providers
240271
* @returns a promise with the application ref
241272
*/
242-
async function hydrate(html: string, component: Type<unknown>, envProviders?: Provider[]):
243-
Promise<ApplicationRef> {
273+
async function hydrate(
274+
html: string, component: Type<unknown>, envProviders?: Provider[],
275+
hydrationFeatures?: HydrationFeatures[]): Promise<ApplicationRef> {
244276
// Destroy existing platform, a new one will be created later by the `bootstrapApplication`.
245277
destroyPlatform();
246278

@@ -257,12 +289,49 @@ describe('platform-server integration', () => {
257289
const providers = [
258290
...(envProviders ?? []),
259291
{provide: DOCUMENT, useFactory: _document, deps: []},
260-
provideHydrationSupport(),
292+
provideClientHydration(...(hydrationFeatures || [])),
261293
];
262294

263295
return bootstrapApplication(component, {providers});
264296
}
265297

298+
describe('public API', () => {
299+
it('should allow to disable DOM hydration using `withoutDomReuse` feature', async () => {
300+
@Component({
301+
standalone: true,
302+
selector: 'app',
303+
template: `
304+
<header>Header</header>
305+
<main>This is hydrated content in the main element.</main>
306+
<footer>Footer</footer>
307+
`,
308+
})
309+
class SimpleComponent {
310+
}
311+
312+
const html =
313+
await ssr(SimpleComponent, undefined, [withDebugConsole()], [withoutDomReuse()]);
314+
const ssrContents = getAppContents(html);
315+
316+
// There should be no `ngh` annotations.
317+
expect(ssrContents).not.toContain(`<app ${NGH_ATTR_NAME}`);
318+
319+
resetTViewsFor(SimpleComponent);
320+
321+
const appRef =
322+
await hydrate(html, SimpleComponent, [withDebugConsole()], [withoutDomReuse()]);
323+
const compRef = getComponentRef<SimpleComponent>(appRef);
324+
appRef.tick();
325+
326+
// Make sure there is no hydration-related message in a console.
327+
verifyHasNoLog(appRef, 'Angular hydrated');
328+
329+
const clientRootNode = compRef.location.nativeElement;
330+
verifyNoNodesWereClaimedForHydration(clientRootNode);
331+
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
332+
});
333+
});
334+
266335
describe('annotations', () => {
267336
it('should add hydration annotations to component host nodes during ssr', async () => {
268337
@Component({

0 commit comments

Comments
 (0)