refactor(core): do not enable hydration when server response was incorrect (#49750)

This commit updates the logic to avoid enabling hydration in case server response doesn't contain hydration-related info serialized. It can happen when `provideClientHydration()` call only happens on the client, but not on the server.

PR Close #49750
This commit is contained in:
Andrew Kushnir
2023-04-07 14:24:37 -07:00
parent a605949093
commit 46ee65ad0f
2 changed files with 58 additions and 33 deletions
+44 -31
View File
@@ -72,27 +72,15 @@ function isBrowser(): boolean {
/**
* Outputs a message with hydration stats into a console.
*/
function printHydrationInfo(injector: Injector) {
function printHydrationStats(injector: Injector) {
const console = injector.get(Console);
const transferState = injector.get(TransferState, null, {optional: true});
if (transferState && transferState.get(NGH_DATA_KEY, null)) {
const message = `Angular hydrated ${ngDevMode!.hydratedComponents} component(s) ` +
`and ${ngDevMode!.hydratedNodes} node(s), ` +
`${ngDevMode!.componentsSkippedHydration} component(s) were skipped. ` +
`Note: this feature is in Developer Preview mode. ` +
`Learn more at https://next.angular.io/guide/hydration.`;
// tslint:disable-next-line:no-console
console.log(message);
} else {
const message = formatRuntimeError(
RuntimeErrorCode.MISSING_HYDRATION_ANNOTATIONS,
'Angular hydration was enabled on the client, but there was no ' +
'serialized information present in the server response. ' +
'Make sure the `provideClientHydration()` is included into the list ' +
'of providers in the server part of the application configuration.');
// tslint:disable-next-line:no-console
console.warn(message);
}
const message = `Angular hydrated ${ngDevMode!.hydratedComponents} component(s) ` +
`and ${ngDevMode!.hydratedNodes} node(s), ` +
`${ngDevMode!.componentsSkippedHydration} component(s) were skipped. ` +
`Note: this feature is in Developer Preview mode. ` +
`Learn more at https://next.angular.io/guide/hydration.`;
// tslint:disable-next-line:no-console
console.log(message);
}
@@ -118,6 +106,32 @@ function whenStable(
*/
export function withDomHydration(): EnvironmentProviders {
return makeEnvironmentProviders([
{
provide: IS_HYDRATION_FEATURE_ENABLED,
useFactory: () => {
if (isBrowser()) {
// On the client, verify that the server response contains
// hydration annotations. Otherwise, keep hydration disabled.
const transferState = inject(TransferState, {optional: true});
const hasHydrationAnnotations = !!transferState?.get(NGH_DATA_KEY, null);
if (!hasHydrationAnnotations) {
const console = inject(Console);
const message = formatRuntimeError(
RuntimeErrorCode.MISSING_HYDRATION_ANNOTATIONS,
'Angular hydration was requested on the client, but there was no ' +
'serialized information present in the server response, ' +
'thus hydration was not enabled. ' +
'Make sure the `provideClientHydration()` is included into the list ' +
'of providers in the server part of the application configuration.');
// tslint:disable-next-line:no-console
console.warn(message);
}
return hasHydrationAnnotations;
}
// We are running on the server, always return `true`.
return true;
},
},
{
provide: ENVIRONMENT_INITIALIZER,
useValue: () => {
@@ -126,27 +140,26 @@ export function withDomHydration(): EnvironmentProviders {
// on the client. Moving forward, the `isBrowser` check should
// be replaced with a tree-shakable alternative (e.g. `isServer`
// flag).
if (isBrowser()) {
if (isBrowser() && inject(IS_HYDRATION_FEATURE_ENABLED)) {
enableHydrationRuntimeSupport();
}
},
multi: true,
},
{
provide: IS_HYDRATION_FEATURE_ENABLED,
useValue: true,
},
{
provide: PRESERVE_HOST_CONTENT,
// Preserve host element content only in a browser
// environment. On a server, an application is rendered
// from scratch, so the host content needs to be empty.
useFactory: () => isBrowser(),
useFactory: () => {
// Preserve host element content only in a browser
// environment and when hydration is configured properly.
// On a server, an application is rendered from scratch,
// so the host content needs to be empty.
return isBrowser() && inject(IS_HYDRATION_FEATURE_ENABLED);
}
},
{
provide: APP_BOOTSTRAP_LISTENER,
useFactory: () => {
if (isBrowser()) {
if (isBrowser() && inject(IS_HYDRATION_FEATURE_ENABLED)) {
const appRef = inject(ApplicationRef);
const pendingTasks = inject(InitialRenderPendingTasks);
const injector = inject(Injector);
@@ -159,7 +172,7 @@ export function withDomHydration(): EnvironmentProviders {
cleanupDehydratedViews(appRef);
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
printHydrationInfo(injector);
printHydrationStats(injector);
}
});
};
@@ -4022,11 +4022,23 @@ describe('platform-server integration', () => {
resetTViewsFor(SimpleComponent);
const appRef = await hydrate(html, SimpleComponent, [withDebugConsole()]);
const compRef = getComponentRef<SimpleComponent>(appRef);
appRef.tick();
verifyHasLog(
appRef,
'NG0505: Angular hydration was enabled on the client, ' +
'but there was no serialized information present in the server response.');
'NG0505: Angular hydration was requested on the client, ' +
'but there was no serialized information present in the server response');
const clientRootNode = compRef.location.nativeElement;
// Make sure that no hydration logic was activated,
// effectively re-rendering from scratch happened and
// all the content inside the <app> host element was
// cleared on the client (as it usually happens in client
// rendering mode).
verifyNoNodesWereClaimedForHydration(clientRootNode);
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
});
});