Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactivity API: Delay block hydration to allow interactive block stores to initialize #66772

Merged
merged 1 commit into from
Nov 6, 2024

Conversation

westonruter
Copy link
Member

Fixes #66691.

When scheduler.yield() is available, the splitTask() function is implemented to use it. When a task is split with scheduler.yield() it results in the tail being added to the front of the task queue. In contrast, when splitTask() is implemented with setTimeout() (when scheduler.yield() is not available), then the tail of the split task is added to the end of the task queue. It appears there is a race condition between the call to init.

In particular, given this init function:

// Initialize the router with the initial DOM.
export const init = async () => {
const nodes = document.querySelectorAll(
`[data-${ directivePrefix }-interactive]`
);
for ( const node of nodes ) {
if ( ! hydratedIslands.has( node ) ) {
await splitTask();
const fragment = getRegionRootFragment( node );
const vdom = toVdom( node );
initialVdom.set( node, vdom );
await splitTask();
hydrate( vdom, fragment );
}
}
};

It is getting invoked before the code in a block's view.js is invoked, for example:

const { state, actions, callbacks } = store(

When splitTask() is was using setTimeout() then the calls to await splitTask() had the effect of delaying the hydration until after the blocks' view modules execute. But when splitTask() is implemented with scheduler.yield() the hydration logic is executing before the interactive blocks' view module (i.e. the store initialization) executes.

Of note, the error also occurs if await splitTask() is eliminated from init function as well, indicating that it was not correctly accounting for delaying the Interactivity API's init until after the interactive blocks have all initialized. There seems to be a dependency problem here, where hydration of an interactive block should be blocked until its corresponding view script module has loaded. This seems to be related to:

In the mean time, adding a setTimeout() to handle the delay seems to do the trick.

Code execution with splitTask implemented with setTimeout

export const init = async () => {
const nodes = document.querySelectorAll(
`[data-${ directivePrefix }-interactive]`
);
for ( const node of nodes ) {
if ( ! hydratedIslands.has( node ) ) {
await splitTask();

const { state, actions, callbacks } = store(

const fragment = getRegionRootFragment( node );
const vdom = toVdom( node );
initialVdom.set( node, vdom );
await splitTask();
hydrate( vdom, fragment );
}
}
};

Code execution with splitTask implemented with scheduler.yield

// Initialize the router with the initial DOM.
export const init = async () => {
const nodes = document.querySelectorAll(
`[data-${ directivePrefix }-interactive]`
);
for ( const node of nodes ) {
if ( ! hydratedIslands.has( node ) ) {
await splitTask();
const fragment = getRegionRootFragment( node );
const vdom = toVdom( node );
initialVdom.set( node, vdom );
await splitTask();
hydrate( vdom, fragment );
}
}
};

const { state, actions, callbacks } = store(

@westonruter westonruter added [Type] Bug An existing feature does not function as intended [Packages] Interactivity /packages/interactivity labels Nov 5, 2024
Copy link

github-actions bot commented Nov 5, 2024

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: westonruter <[email protected]>
Co-authored-by: cbravobernal <[email protected]>
Co-authored-by: michalczaplinski <[email protected]>
Co-authored-by: annezazu <[email protected]>
Co-authored-by: artemiomorales <[email protected]>
Co-authored-by: t-hamano <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@cbravobernal cbravobernal added the Backport to Gutenberg RC Pull request that needs to be backported to a Gutenberg release candidate (RC) label Nov 6, 2024
@cbravobernal cbravobernal added this to the Gutenberg 19.6 milestone Nov 6, 2024
@michalczaplinski
Copy link
Contributor

Thanks for investigating and creating the PR @westonruter ! 🙂

The current solution does fix the problem. However, even with the current workaround, the issue will persist if I remove or comment out the splitTask () calls in init(). I think it's not optimal that we have an implicit dependency on splitTask() which should just be a performance optimization.

I'm currently trying to figure out a better way to handle this.

Copy link
Contributor

@cbravobernal cbravobernal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As @michalczaplinski commented, we can work in a follow-up PR to find a better solution. Meanwhile, I'll merge it and include it on today's Gutenberg release.

@cbravobernal cbravobernal merged commit 624c938 into trunk Nov 6, 2024
72 of 73 checks passed
@michalczaplinski michalczaplinski self-requested a review November 6, 2024 14:44
@cbravobernal cbravobernal deleted the fix/split-task-with-scheduler-yield branch November 6, 2024 14:44
@cbravobernal
Copy link
Contributor

I just cherry-picked this PR to the release/19.6 branch to get it included in the next release: f4b013f

cbravobernal added a commit that referenced this pull request Nov 6, 2024
Co-authored-by: westonruter <[email protected]>
Co-authored-by: michalczaplinski <[email protected]>
Co-authored-by: annezazu <[email protected]>
Co-authored-by: artemiomorales <[email protected]>
Co-authored-by: cbravobernal <[email protected]>
Co-authored-by: t-hamano <[email protected]>
@cbravobernal cbravobernal removed the Backport to Gutenberg RC Pull request that needs to be backported to a Gutenberg release candidate (RC) label Nov 6, 2024
@cbravobernal cbravobernal changed the title Delay block hydration to allow interactive block stores to initialize Interactivity API: Delay block hydration to allow interactive block stores to initialize Nov 6, 2024
@westonruter
Copy link
Member Author

The current solution does fix the problem. However, even with the current workaround, the issue will persist if I remove or comment out the splitTask () calls in init(). I think it's not optimal that we have an implicit dependency on splitTask() which should just be a performance optimization.

@michalczaplinski Really? I commented out the splitTask() calls and I was getting the error still. I just checked again, and I continue to get the errors. With this merged PR, I commented out these lines:

diff --git a/packages/interactivity/src/init.ts b/packages/interactivity/src/init.ts
index fa1eec51c3e..adc17a6458e 100644
--- a/packages/interactivity/src/init.ts
+++ b/packages/interactivity/src/init.ts
@@ -40,17 +40,17 @@ export const init = async () => {
 	 * This occurs when splitTask() is implemented with scheduler.yield() as opposed to setTimeout(), as with the former
 	 * split tasks are added to the front of the task queue whereas with the latter they are added to the end of the queue.
 	 */
-	await new Promise( ( resolve ) => {
-		setTimeout( resolve, 0 );
-	} );
+	// await new Promise( ( resolve ) => {
+	// 	setTimeout( resolve, 0 );
+	// } );
 
 	for ( const node of nodes ) {
 		if ( ! hydratedIslands.has( node ) ) {
-			await splitTask();
+			// await splitTask();
 			const fragment = getRegionRootFragment( node );
 			const vdom = toVdom( node );
 			initialVdom.set( node, vdom );
-			await splitTask();
+			// await splitTask();
 			hydrate( vdom, fragment );
 		}
 	}

And I get these errors:

image

@michalczaplinski
Copy link
Contributor

@westonruter, I'm unsure what you mean 🙂. I think that we're in agreement that the Cannot access 'state'... issue persists even if you comment out the splitTask() calls in init(). This is what I meant with my previous comment.

@westonruter
Copy link
Member Author

@michalczaplinski Oh, hah, sorry! I misunderstood.

@michalczaplinski
Copy link
Contributor

michalczaplinski commented Nov 7, 2024

Sorry, I should have been more clear earlier too 🙂 . Here's my expanded explanation with additional details:

Adding await new Promise(...) inside the init() (which was done in the current PR) fixes #66691. This fix ALSO works even if you comment out the splitTask() calls.

However, relying on await new Promise(...) inside the init() can be brittle. Imagine a custom block adding another await new Promise(...) at the module level in its view.js file. This will cause the same problem to reappear because now the individual view.js file adds a task to the end of the task queue.

I'm not sure if there's a better solution at this point than what was merged in the current PR. @DAreRodz and I have considered wrapping the init() with DOMContentloaded as it was removed here, but that does not solve the problem either.

@westonruter
Copy link
Member Author

I'm not sure if there's a better solution at this point than what was merged in the current PR. @DAreRodz and I have considered wrapping the init() with DOMContentloaded as it was removed here, but that does not solve the problem either.

AFAIK, the DOMContentLoaded event won't work specifically because these are script modules, and script modules execute after this event. So if any script module tries to add a DOMContentLoaded event, the event will never fire.

@michalczaplinski
Copy link
Contributor

AFAIK, the DOMContentLoaded event won't work specifically because these are script modules, and script modules execute after this event. So if any script module tries to add a DOMContentLoaded event, the event will never fire.

right, exactly 🙂

karthick-murugan pushed a commit to karthick-murugan/gutenberg that referenced this pull request Nov 13, 2024
Co-authored-by: westonruter <[email protected]>
Co-authored-by: michalczaplinski <[email protected]>
Co-authored-by: annezazu <[email protected]>
Co-authored-by: artemiomorales <[email protected]>
Co-authored-by: cbravobernal <[email protected]>
Co-authored-by: t-hamano <[email protected]>
@luisherranz
Copy link
Member

@DAreRodz let's take a look at this for WordPress 6.8 to see if we can ensure there are no issues when stores initialize after the init function is called, which is necessary for future implementations, like lazy loading blocks.

That being said, it's okay for the blocks that load initially to do so before the init function, as it is happening today (because we can optimize a few things in the init function itself).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Packages] Interactivity /packages/interactivity [Type] Bug An existing feature does not function as intended
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Image Block: Lightbox does not work
4 participants