@@ -7,7 +7,7 @@ import 'requestidlecallback';
77 * Internal dependencies
88 */
99import { toVdom , hydratedIslands } from './vdom' ;
10- import { createRootFragment , splitTask } from './utils' ;
10+ import { createRootFragment , splitTask , warn } from './utils' ;
1111
1212// Keep the same root fragment for each interactive region node.
1313const regionRootFragments = new WeakMap ( ) ;
@@ -60,12 +60,19 @@ const hydrateNode = async ( node: Element ) => {
6060 if ( hydratedIslands . has ( node ) ) {
6161 return ;
6262 }
63- const fragment = getRegionRootFragment ( node ) ;
64- const vdom = toVdom ( node ) ;
65- initialVdom . set ( node , vdom ) ;
66- await splitTask ( ) ;
67- hydrate ( vdom , fragment ) ;
68- await splitTask ( ) ;
63+ try {
64+ const fragment = getRegionRootFragment ( node ) ;
65+ const vdom = toVdom ( node ) ;
66+ initialVdom . set ( node , vdom ) ;
67+ await splitTask ( ) ;
68+ hydrate ( vdom , fragment ) ;
69+ } catch ( e ) {
70+ warn (
71+ `Failed to hydrate island: ${ ( e as Error ) . message ?? e } `
72+ ) ;
73+ } finally {
74+ await splitTask ( ) ;
75+ }
6976} ;
7077
7178/**
@@ -88,7 +95,7 @@ export const hydrateAllRemaining = async () => {
8895 intersectionObserver ?. disconnect ( ) ;
8996 // Hydrate every island currently in the DOM that isn't hydrated yet.
9097 for ( const node of document . querySelectorAll ( '[data-wp-interactive]' ) ) {
91- await hydrateNode ( node ) ;
98+ await hydrateNode ( node as Element ) ;
9299 }
93100 resolveInitialVdom ( initialVdom ) ;
94101} ;
@@ -121,20 +128,25 @@ export const hydrateRegions = async () => {
121128 continue ;
122129 }
123130
124- const node = entry . target ;
131+ const node = entry . target as Element ;
125132 intersectionObserverInstance . unobserve ( node ) ;
126133 observedNodes . delete ( node ) ;
127-
134+ // `hydrateNode` handles its own errors, so a single
135+ // failing island cannot abort the rest of the batch.
128136 await hydrateNode ( node ) ;
137+ }
129138
130- if ( observedNodes . size === 0 ) {
131- intersectionObserverInstance . disconnect ( ) ;
132- // All islands have been hydrated: resolve the promise with
133- // the fully populated initialVdom so the router can start
134- // doing the DOM diffing between the previous and next pages.
135- idleFired = true ; // Disarm the idle-time sweep.
136- resolveInitialVdom ( initialVdom ) ;
137- }
139+ // All observed islands have been hydrated: resolve the promise
140+ // with the fully populated initialVdom so the router can start
141+ // doing the DOM diffing between the previous and next pages.
142+ // Resolving AFTER the batch's hydration attempts (rather than
143+ // before the last node's hydrate) guarantees the router never
144+ // reads a partially hydrated island list.
145+ if ( observedNodes . size === 0 ) {
146+ intersectionObserverInstance . disconnect ( ) ;
147+ // Disarm the idle-time sweep.
148+ idleFired = true ;
149+ resolveInitialVdom ( initialVdom ) ;
138150 }
139151 } ,
140152 {
0 commit comments