-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
useInnerBlockTemplateSync: cancel template sync on innerBlocks change or unmount #46307
Conversation
Size Change: +25 B (0%) Total Size: 1.32 MB
ℹ️ View Unchanged
|
// Only synchronize innerBlocks with template if innerBlocks are empty | ||
// or a locking "all" or "contentOnly" exists directly on the block. | ||
const currentInnerBlocks = getBlocks( clientId ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure there was a bug that forced me to use this selector here. In other words, something updated the inner blocks between the scheduling of the micro task and its execution. I can't recall exactly though and looking at the PR that introduced the microtask, it's not clear either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This approach also allows us to avoid reading currentInnerBlocks. Cancelling provides a guarantee that the innerBlocks from the closure are always valid.
It allows this only if the "effect cleanup" is triggered before the microtask. Is this a given?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a given?
Thanks for raising this. I investigated on a little toy project and the answer is that with a correct implementation of useSelect
, yes, it should be a given. When the canonical store value changes, a store listener is called, useSelect
calls forceRender()
(which is an alias for setState
), and that setState
performs effect cleanup. If all of that happens synchronously within one event loop tick, then there is no room for another microtask to change the value.
Whether it happens synchronously depends on the exact setup of the environment. React 18 with legacy render
(React 17) is sync, React 18 with createRoot
is not. We'd have to use useSyncExternalStore
in useSelect
to make it sync again.
I'll try to rewrite the implementation to be more resilient and not require complex reasoning about React scheduling to prove that it's correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some experiments also with useSyncExternalStore
, use[Layout]Effect
and queueMicrotask
, and the conclusion is that innerBlocks
and currentInnerBlocks
can get out of sync quite easily. So I reverted the part of my patch where I removed currentInnerBlocks
.
Now the patch just cancels the scheduled microtask every time the effect is cleaned up. Either on unmount, or when relevant data (inner blocks or template lock) change.
b68c5c4
to
4e0f34c
Compare
This fixes a little bug in
useInnerBlockTemplateSync
that surfaces when running this React Native test:gutenberg/packages/block-library/src/buttons/test/edit.native.js
Lines 205 to 241 in c8516e6
The test creates a "Buttons" block and then it immediately and synchronously removes it. But meanwhile,
useInnerBlockTemplateSync
scheduled a microtask to populate the no-longer-existing block with the default template, i.e., one inner button. This microtask runs when the "Buttons" block no longer exists, dispatching thereplaceInnerBlocks
action on an invalidclientId
. In the test, this triggers a "update not wrapped inact()
" warning.I'm fixing this by cancelling the microtask when the effect dependencies change (mainly
innerBlocks
) or when the component is unmounted. Only when theinnerBlocks
value remains unchanged for at least one microtask tick, do we perform the sync.This approach also allows us to avoid reading
currentInnerBlocks
. Cancelling provides a guarantee that theinnerBlocks
from the closure are always valid.