diff --git a/packages/core/src/hydration/api.ts b/packages/core/src/hydration/api.ts index 42101635260..264b19c4290 100644 --- a/packages/core/src/hydration/api.ts +++ b/packages/core/src/hydration/api.ts @@ -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); } }); }; diff --git a/packages/platform-server/test/hydration_spec.ts b/packages/platform-server/test/hydration_spec.ts index b7d8689fc5f..afbcc77abb4 100644 --- a/packages/platform-server/test/hydration_spec.ts +++ b/packages/platform-server/test/hydration_spec.ts @@ -4022,11 +4022,23 @@ describe('platform-server integration', () => { resetTViewsFor(SimpleComponent); const appRef = await hydrate(html, SimpleComponent, [withDebugConsole()]); + const compRef = getComponentRef(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 host element was + // cleared on the client (as it usually happens in client + // rendering mode). + verifyNoNodesWereClaimedForHydration(clientRootNode); + verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); }); });