Skip to content

Commit 52b8e07

Browse files
SkyZeroZxcrisbeto
authored andcommitted
feat(platform-browser): Warns on conflicting hydration and blocking navigation (#62963)
Adds an internal token to detect when both hydration and blocking initial navigation are enabled. Logs a warning during app initialization if this unsupported combination is found, helping developers avoid misconfiguration and potential runtime issues. PR Close #62963
1 parent 8f414b2 commit 52b8e07

File tree

4 files changed

+48
-6
lines changed

4 files changed

+48
-6
lines changed

packages/core/src/core_private_export.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export {
9393
IS_HYDRATION_DOM_REUSE_ENABLED as ɵIS_HYDRATION_DOM_REUSE_ENABLED,
9494
IS_INCREMENTAL_HYDRATION_ENABLED as ɵIS_INCREMENTAL_HYDRATION_ENABLED,
9595
JSACTION_BLOCK_ELEMENT_MAP as ɵJSACTION_BLOCK_ELEMENT_MAP,
96+
IS_ENABLED_BLOCKING_INITIAL_NAVIGATION as ɵIS_ENABLED_BLOCKING_INITIAL_NAVIGATION,
9697
} from './hydration/tokens';
9798
export {
9899
HydrationStatus as ɵHydrationStatus,

packages/core/src/hydration/tokens.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {InjectionToken} from '../di/injection_token';
1313
* during hydration is enabled.
1414
*/
1515
export const IS_HYDRATION_DOM_REUSE_ENABLED = new InjectionToken<boolean>(
16-
typeof ngDevMode === 'undefined' || !!ngDevMode ? 'IS_HYDRATION_DOM_REUSE_ENABLED' : '',
16+
typeof ngDevMode === 'undefined' || ngDevMode ? 'IS_HYDRATION_DOM_REUSE_ENABLED' : '',
1717
);
1818

1919
// By default (in client rendering mode), we remove all the contents
@@ -25,7 +25,7 @@ export const PRESERVE_HOST_CONTENT_DEFAULT = false;
2525
* retained during the bootstrap.
2626
*/
2727
export const PRESERVE_HOST_CONTENT = new InjectionToken<boolean>(
28-
typeof ngDevMode === 'undefined' || !!ngDevMode ? 'PRESERVE_HOST_CONTENT' : '',
28+
typeof ngDevMode === 'undefined' || ngDevMode ? 'PRESERVE_HOST_CONTENT' : '',
2929
{
3030
providedIn: 'root',
3131
factory: () => PRESERVE_HOST_CONTENT_DEFAULT,
@@ -37,15 +37,15 @@ export const PRESERVE_HOST_CONTENT = new InjectionToken<boolean>(
3737
* is enabled.
3838
*/
3939
export const IS_I18N_HYDRATION_ENABLED = new InjectionToken<boolean>(
40-
typeof ngDevMode === 'undefined' || !!ngDevMode ? 'IS_I18N_HYDRATION_ENABLED' : '',
40+
typeof ngDevMode === 'undefined' || ngDevMode ? 'IS_I18N_HYDRATION_ENABLED' : '',
4141
);
4242

4343
/**
4444
* Internal token that indicates whether event replay support for SSR
4545
* is enabled.
4646
*/
4747
export const IS_EVENT_REPLAY_ENABLED = new InjectionToken<boolean>(
48-
typeof ngDevMode === 'undefined' || !!ngDevMode ? 'IS_EVENT_REPLAY_ENABLED' : '',
48+
typeof ngDevMode === 'undefined' || ngDevMode ? 'IS_EVENT_REPLAY_ENABLED' : '',
4949
);
5050

5151
export const EVENT_REPLAY_ENABLED_DEFAULT = false;
@@ -55,7 +55,7 @@ export const EVENT_REPLAY_ENABLED_DEFAULT = false;
5555
* is enabled.
5656
*/
5757
export const IS_INCREMENTAL_HYDRATION_ENABLED = new InjectionToken<boolean>(
58-
typeof ngDevMode === 'undefined' || !!ngDevMode ? 'IS_INCREMENTAL_HYDRATION_ENABLED' : '',
58+
typeof ngDevMode === 'undefined' || ngDevMode ? 'IS_INCREMENTAL_HYDRATION_ENABLED' : '',
5959
);
6060

6161
/**
@@ -68,3 +68,10 @@ export const JSACTION_BLOCK_ELEMENT_MAP = new InjectionToken<Map<string, Set<Ele
6868
factory: () => new Map<string, Set<Element>>(),
6969
},
7070
);
71+
72+
/**
73+
* Internal token that indicates whether the initial navigation is blocking in the application.
74+
*/
75+
export const IS_ENABLED_BLOCKING_INITIAL_NAVIGATION = new InjectionToken<boolean>(
76+
typeof ngDevMode === 'undefined' || ngDevMode ? 'IS_ENABLED_BLOCKING_INITIAL_NAVIGATION' : '',
77+
);

packages/platform-browser/src/hydration.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import {
2222
ɵwithI18nSupport,
2323
ɵZONELESS_ENABLED as ZONELESS_ENABLED,
2424
ɵwithIncrementalHydration,
25+
ɵIS_ENABLED_BLOCKING_INITIAL_NAVIGATION as IS_ENABLED_BLOCKING_INITIAL_NAVIGATION,
2526
} from '@angular/core';
26-
2727
import {RuntimeErrorCode} from './errors';
2828

2929
/**
@@ -170,6 +170,35 @@ function provideZoneJsCompatibilityDetector(): Provider[] {
170170
];
171171
}
172172

173+
/**
174+
* Returns an `ENVIRONMENT_INITIALIZER` token setup with a function
175+
* that verifies whether enabledBlocking initial navigation is used in an application
176+
* and logs a warning in a console if it's not compatible with hydration.
177+
*/
178+
function provideEnabledBlockingInitialNavigationDetector(): Provider[] {
179+
return [
180+
{
181+
provide: ENVIRONMENT_INITIALIZER,
182+
useValue: () => {
183+
const isEnabledBlockingInitialNavigation = inject(IS_ENABLED_BLOCKING_INITIAL_NAVIGATION, {
184+
optional: true,
185+
});
186+
187+
if (isEnabledBlockingInitialNavigation) {
188+
const console = inject(Console);
189+
const message = formatRuntimeError(
190+
RuntimeErrorCode.HYDRATION_CONFLICTING_FEATURES,
191+
'Configuration error: found both hydration and enabledBlocking initial navigation ' +
192+
'in the same application, which is a contradiction.',
193+
);
194+
console.warn(message);
195+
}
196+
},
197+
multi: true,
198+
},
199+
];
200+
}
201+
173202
/**
174203
* Sets up providers necessary to enable hydration functionality for the application.
175204
*
@@ -251,6 +280,9 @@ export function provideClientHydration(
251280

252281
return makeEnvironmentProviders([
253282
typeof ngDevMode !== 'undefined' && ngDevMode ? provideZoneJsCompatibilityDetector() : [],
283+
typeof ngDevMode !== 'undefined' && ngDevMode
284+
? provideEnabledBlockingInitialNavigationDetector()
285+
: [],
254286
withDomHydration(),
255287
featuresKind.has(HydrationFeatureKind.NoHttpTransferCache) || hasHttpTransferCacheOptions
256288
? []

packages/router/src/provide_router.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
runInInjectionContext,
2929
Type,
3030
ɵperformanceMarkFeature as performanceMarkFeature,
31+
ɵIS_ENABLED_BLOCKING_INITIAL_NAVIGATION as IS_ENABLED_BLOCKING_INITIAL_NAVIGATION,
3132
} from '@angular/core';
3233
import {of, Subject} from 'rxjs';
3334

@@ -347,6 +348,7 @@ export type InitialNavigationFeature =
347348
*/
348349
export function withEnabledBlockingInitialNavigation(): EnabledBlockingInitialNavigationFeature {
349350
const providers = [
351+
{provide: IS_ENABLED_BLOCKING_INITIAL_NAVIGATION, useValue: true},
350352
{provide: INITIAL_NAVIGATION, useValue: InitialNavigation.EnabledBlocking},
351353
provideAppInitializer(() => {
352354
const injector = inject(Injector);

0 commit comments

Comments
 (0)