Skip to content

Commit 2225444

Browse files
AndrewKushnirdevversion
authored andcommitted
refactor(core): avoid hydration warnings when RenderMode.Client is set (angular#58004)
With the newly-added `RenderMode` config for routes, some of the routes may have the `RenderMode.Client` mode enabled, while also having `provideClientHydration()` function in provider list at bootstrap. As a result, there was a false-positive warning in a console, notifying developers about hydration misconfiguration. This commit adds extra logic to handle this situation and avoid such warnings. Note: there is a change required on the CLI side to add an extra marker, which would activate the logic added in this commit. PR Close angular#58004
1 parent a31721a commit 2225444

File tree

3 files changed

+110
-30
lines changed

3 files changed

+110
-30
lines changed

packages/core/src/hydration/api.ts

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import {cleanupDehydratedViews} from './cleanup';
3434
import {
3535
enableClaimDehydratedIcuCaseImpl,
3636
enablePrepareI18nBlockForHydrationImpl,
37-
isI18nHydrationEnabled,
3837
setIsI18nHydrationSupportEnabled,
3938
} from './i18n';
4039
import {
@@ -143,6 +142,21 @@ function whenStableWithTimeout(appRef: ApplicationRef, injector: Injector): Prom
143142
return whenStablePromise;
144143
}
145144

145+
/**
146+
* Defines a name of an attribute that is added to the <body> tag
147+
* in the `index.html` file in case a given route was configured
148+
* with `RenderMode.Client`. 'cm' is an abbreviation for "Client Mode".
149+
*/
150+
export const CLIENT_RENDER_MODE_FLAG = 'ngcm';
151+
152+
/**
153+
* Checks whether the `RenderMode.Client` was defined for the current route.
154+
*/
155+
function isClientRenderModeEnabled(): boolean {
156+
const doc = getDocument();
157+
return isPlatformBrowser() && doc.body.hasAttribute(CLIENT_RENDER_MODE_FLAG);
158+
}
159+
146160
/**
147161
* Returns a set of providers required to setup hydration support
148162
* for an application that is server side rendered. This function is
@@ -164,19 +178,6 @@ export function withDomHydration(): EnvironmentProviders {
164178
// hydration annotations. Otherwise, keep hydration disabled.
165179
const transferState = inject(TransferState, {optional: true});
166180
isEnabled = !!transferState?.get(NGH_DATA_KEY, null);
167-
if (!isEnabled && typeof ngDevMode !== 'undefined' && ngDevMode) {
168-
const console = inject(Console);
169-
const message = formatRuntimeError(
170-
RuntimeErrorCode.MISSING_HYDRATION_ANNOTATIONS,
171-
'Angular hydration was requested on the client, but there was no ' +
172-
'serialized information present in the server response, ' +
173-
'thus hydration was not enabled. ' +
174-
'Make sure the `provideClientHydration()` is included into the list ' +
175-
'of providers in the server part of the application configuration.',
176-
);
177-
// tslint:disable-next-line:no-console
178-
console.warn(message);
179-
}
180181
}
181182
if (isEnabled) {
182183
performanceMarkFeature('NgHydration');
@@ -191,14 +192,29 @@ export function withDomHydration(): EnvironmentProviders {
191192
// no way to turn it off (e.g. for tests), so we turn it off by default.
192193
setIsI18nHydrationSupportEnabled(false);
193194

194-
// Since this function is used across both server and client,
195-
// make sure that the runtime code is only added when invoked
196-
// on the client. Moving forward, the `isPlatformBrowser` check should
197-
// be replaced with a tree-shakable alternative (e.g. `isServer`
198-
// flag).
199-
if (isPlatformBrowser() && inject(IS_HYDRATION_DOM_REUSE_ENABLED)) {
195+
if (!isPlatformBrowser()) {
196+
// Since this function is used across both server and client,
197+
// make sure that the runtime code is only added when invoked
198+
// on the client (see the `enableHydrationRuntimeSupport` function
199+
// call below).
200+
return;
201+
}
202+
203+
if (inject(IS_HYDRATION_DOM_REUSE_ENABLED)) {
200204
verifySsrContentsIntegrity();
201205
enableHydrationRuntimeSupport();
206+
} else if (typeof ngDevMode !== 'undefined' && ngDevMode && !isClientRenderModeEnabled()) {
207+
const console = inject(Console);
208+
const message = formatRuntimeError(
209+
RuntimeErrorCode.MISSING_HYDRATION_ANNOTATIONS,
210+
'Angular hydration was requested on the client, but there was no ' +
211+
'serialized information present in the server response, ' +
212+
'thus hydration was not enabled. ' +
213+
'Make sure the `provideClientHydration()` is included into the list ' +
214+
'of providers in the server part of the application configuration.',
215+
);
216+
// tslint:disable-next-line:no-console
217+
console.warn(message);
202218
}
203219
},
204220
multi: true,
@@ -250,14 +266,16 @@ export function withI18nSupport(): Provider[] {
250266
return [
251267
{
252268
provide: IS_I18N_HYDRATION_ENABLED,
253-
useValue: true,
269+
useFactory: () => inject(IS_HYDRATION_DOM_REUSE_ENABLED),
254270
},
255271
{
256272
provide: ENVIRONMENT_INITIALIZER,
257273
useValue: () => {
258-
enableI18nHydrationRuntimeSupport();
259-
setIsI18nHydrationSupportEnabled(true);
260-
performanceMarkFeature('NgI18nHydration');
274+
if (inject(IS_HYDRATION_DOM_REUSE_ENABLED)) {
275+
enableI18nHydrationRuntimeSupport();
276+
setIsI18nHydrationSupportEnabled(true);
277+
performanceMarkFeature('NgI18nHydration');
278+
}
261279
},
262280
multi: true,
263281
},

packages/platform-server/test/dom_utils.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
*/
88

99
import {DOCUMENT} from '@angular/common';
10-
import {ApplicationRef, Provider, Type, ɵsetDocument} from '@angular/core';
10+
import {ApplicationRef, PLATFORM_ID, Provider, Type, ɵsetDocument} from '@angular/core';
11+
import {CLIENT_RENDER_MODE_FLAG} from '@angular/core/src/hydration/api';
1112
import {getComponentDef} from '@angular/core/src/render3/definition';
1213
import {
1314
bootstrapApplication,
@@ -102,6 +103,7 @@ export function hydrate(
102103
const hydrationFeatures = options?.hydrationFeatures ?? [];
103104
const providers = [
104105
...envProviders,
106+
{provide: PLATFORM_ID, useValue: 'browser'},
105107
{provide: DOCUMENT, useFactory: _document, deps: []},
106108
provideClientHydration(...hydrationFeatures),
107109
];
@@ -112,6 +114,14 @@ export function hydrate(
112114
export function render(doc: Document, html: string) {
113115
// Get HTML contents of the `<app>`, create a DOM element and append it into the body.
114116
const container = convertHtmlToDom(html, doc);
117+
118+
// If there was a client render mode marker present in HTML - apply it to the <body>
119+
// element as well.
120+
const hasClientModeMarker = new RegExp(` ${CLIENT_RENDER_MODE_FLAG}`, 'g').test(html);
121+
if (hasClientModeMarker) {
122+
doc.body.setAttribute(CLIENT_RENDER_MODE_FLAG, '');
123+
}
124+
115125
Array.from(container.childNodes).forEach((node) => doc.body.appendChild(node));
116126
}
117127

@@ -136,3 +146,11 @@ export async function renderAndHydrate(
136146
render(doc, html);
137147
return hydrate(doc, component, options);
138148
}
149+
150+
/**
151+
* Clears document contents to have a clean state for the next test.
152+
*/
153+
export function clearDocument(doc: Document) {
154+
doc.body.textContent = '';
155+
doc.body.removeAttribute(CLIENT_RENDER_MODE_FLAG);
156+
}

packages/platform-server/test/hydration_spec.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,14 @@ import {provideRouter, RouterOutlet, Routes} from '@angular/router';
7272
import {provideServerRendering} from '../public_api';
7373
import {renderApplication} from '../src/utils';
7474

75-
import {getAppContents, renderAndHydrate, resetTViewsFor, stripUtilAttributes} from './dom_utils';
75+
import {
76+
clearDocument,
77+
getAppContents,
78+
renderAndHydrate,
79+
resetTViewsFor,
80+
stripUtilAttributes,
81+
} from './dom_utils';
82+
import {CLIENT_RENDER_MODE_FLAG} from '@angular/core/src/hydration/api';
7683

7784
/**
7885
* The name of the attribute that contains a slot index
@@ -214,6 +221,17 @@ function verifyHasNoLog(appRef: ApplicationRef, message: string) {
214221
.toBe(false);
215222
}
216223

224+
/**
225+
* Verifies that there are no messages in a console.
226+
*/
227+
function verifyEmptyConsole(appRef: ApplicationRef) {
228+
const console = appRef.injector.get(Console) as DebugConsole;
229+
const logs = console.logs.filter(
230+
(msg) => !msg.startsWith('Angular is running in development mode'),
231+
);
232+
expect(logs).toEqual([]);
233+
}
234+
217235
function getHydrationInfoFromTransferState(input: string): string | undefined {
218236
return input.match(/<script[^>]+>(.*?)<\/script>/)?.[1];
219237
}
@@ -271,9 +289,7 @@ describe('platform-server hydration integration', () => {
271289
doc = TestBed.inject(DOCUMENT);
272290
});
273291

274-
afterEach(() => {
275-
doc.body.textContent = '';
276-
});
292+
afterEach(() => clearDocument(doc));
277293

278294
/**
279295
* This renders the application with server side rendering logic.
@@ -7079,7 +7095,7 @@ describe('platform-server hydration integration', () => {
70797095
}
70807096
});
70817097

7082-
it('should log an warning when there was no hydration info in the TransferState', async () => {
7098+
it('should log a warning when there was no hydration info in the TransferState', async () => {
70837099
@Component({
70847100
standalone: true,
70857101
selector: 'app',
@@ -7117,6 +7133,34 @@ describe('platform-server hydration integration', () => {
71177133
verifyNoNodesWereClaimedForHydration(clientRootNode);
71187134
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
71197135
});
7136+
7137+
it(
7138+
'should not log a warning when there was no hydration info in the TransferState, ' +
7139+
'but a client mode marker is present',
7140+
async () => {
7141+
@Component({
7142+
standalone: true,
7143+
selector: 'app',
7144+
template: `Hi!`,
7145+
})
7146+
class SimpleComponent {}
7147+
7148+
const html = `<html><head></head><body ${CLIENT_RENDER_MODE_FLAG}><app></app></body></html>`;
7149+
7150+
resetTViewsFor(SimpleComponent);
7151+
7152+
const appRef = await renderAndHydrate(doc, html, SimpleComponent, {
7153+
envProviders: [withDebugConsole()],
7154+
});
7155+
const compRef = getComponentRef<SimpleComponent>(appRef);
7156+
appRef.tick();
7157+
7158+
verifyEmptyConsole(appRef);
7159+
7160+
const clientRootNode = compRef.location.nativeElement;
7161+
expect(clientRootNode.textContent).toContain('Hi!');
7162+
},
7163+
);
71207164
});
71217165

71227166
describe('@if', () => {

0 commit comments

Comments
 (0)