-
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
useBlockRefs: use more efficient lookup map, use uSES #60945
Conversation
Size Change: -10 B (0%) Total Size: 1.75 MB
ℹ️ View Unchanged
|
map.set( id, setForId ); | ||
} | ||
setForId.add( value ); | ||
} |
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.
Any reason to not reuse the same logic from #60879?
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.
The situation is very similar, but not entirely identical. Here, when adding refs to the map, we are merely registering them, without firing listeners. It's only when the ref callback actually sets the ref.current
when the listeners are fired.
Maybe it's possible to merge the two implementations, but I'd rather do it in steps.
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 If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.
To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
||
function getRefElement( refs, clientId ) { | ||
// Multiple refs may be created for a single block. Find the | ||
// first that has an element set. |
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 don't remember why there were multiple refs 😬
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.
There are multiple refs if you call useBlockRefProvider( clientId )
twice for the same clientId
:
const refA = useBlockRefProvider( clientId );
return <div ref={ refA } />;
// somewhere else, with same clientId but different element
const refB = useBlockRefProvider( clientId );
return <div ref={ refB } />;
The it registers two refs for the same clientId
, and useBlockRef
/useBlockElement
will return the one that's non-null.
I also don't know if this ever happens 🙂
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 wonder if we should just error if we try to call it twice with the same 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.
Yes, ignoring the second call and printing a warning would certainly simplify the code. Then we can store just one element instead of a Set
.
return () => deleteFromMap( refs, clientId, ref ); | ||
}, [ refs, clientId ] ); | ||
|
||
return useCallback( |
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.
Can't we reuse the same callback for the layout effect as well? The benefit of useRefEffect
is that you can pass a cleanup function just like useLayoutEffect
, so I think we could add/remove to/from the map as well in addition to callListeners
?
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.
What do you mean by reuse?
I looked more closely at the lifecycle of this hook and some things are suspicious:
- On mount, the DOM element is created, and the ref callback called. This callback sets the
ref.current
and calls listeners. But at this moment, the effect that adds theref
to the map hasn't yet run, so the listeners don't see the element yet!useBlockElement( clientId )
hooks that started observingclientId
before it was mounted will getnull
as return value. That looks bad! Intrunk
this works ok because the element is set directly with localsetElement
state setter, the map is not used. Only this branch is broken. - Now the effect that calls
addToMap
runs. But there's no calling of listeners. ObservinguseBlockElement
hooks won't be notified. - What if, during the lifecycle of the component, the reffed element changes? Consider code like this:
When the element changes like this, the
const ref = useBlockRefProvider( clientId ); return variant ? <b ref={ ref } /> : <i ref={ ref }>;
ref
callback will be first called withnull
. But our callback ignoresnull
values, so it will do nothing. Then theref
callback is called with the new element. That updates theref.current
and notifies the listeners, and they will see the new element. That's great. - When the component is unmounting, first the
ref
callback is called withnull
. Nothing happens, listeners are not notified. - Then the cleanup callback of the effect is called and the
ref
is removed from the map. - If a component that calls
useBlockElement
is rerendered for a random reason, then i) intrunk
it will return the element that no longer exists, because it's stored in the hook's local state; ii) in this branch it will returnnull
because thegetValue
function foruSES
won't find the map record. In both branches the hook won't be notified about the element change, we need to wait for a random rerender triggered by something else.
To conclude, this PR still needs more testing and more work.
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.
That's what I meant here: addToMap
should move to the ref callback, right before callListeners
? The ref callback will also be called when then a ref is attached to a newly mounted component (your snipper), so the ref will be re-added and listeners called again.
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.
addToMap
should move to the ref callback
The disadvantage of this is that when the element changes, the listeners will get two notifications: one about null
element and one about the new element. While ideally they should be notified only about the new element. But it's an edge case anyway, and it's not harmful, just slightly inefficient.
packages/block-editor/src/components/block-list/use-block-props/use-block-refs.js
Show resolved
Hide resolved
e3bf6ca
to
76082e5
Compare
@ellatrix I pushed a 76082e5 commit which radically simplifies the implementation 🙂 It uses the
If you mount multiple
const { get } = useSelect( 'store' );
function onClick() {
const el = get();
} in the sense that it's just a non-reactive getter, and the getter is hidden in the TODO: decide whether |
76082e5
to
4c48dcb
Compare
I agree
IMHO, we should err on the side of caution with these new APIs and leave them private for now until we find more use cases. That said, for this one it's easier to deprecate and create a new one if needed, but I still think it's nice to find a few more other uses? |
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 is awesome, exactly what I was hoping for with useObservableValue
🙂
4c48dcb
to
3e74736
Compare
After trying to make the new The main reason because private APIs have trouble with types. I need to export an import type { ObservableMap } from '@wordpress/compose';
type SlotFillContext = {
slots: ObservableMap<SlotKey, SlotValue>;
}; But the type can't be private, only the JS exports can! Additionally, the On the other hand, both I pushed a commit that improves the JSDoc documentation, and I'm ready to merge. |
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.
Sounds good to me as a public API. 👍
Perhaps we'll find more good usages for it eventually.
Thanks @jsnajdr this is stellar work 🚀
Sounds good! |
* useBlockRefs: use more efficient lookup map, use uSES * Rewrite block refs with observableMap, which moves to compose * Improve docs * Add changelog entry
Updates the
useBlockRefs
implementation in two ways:refs
andcallbacks
was effectively an array, always searched by linear traversal. The.get
method was never used! Now it's a real map indexed byclientId
and storingSet
of refs or listeners.useSyncExternalStore
instead of localuseState
to reactively observe ref elements being added and removed.Let's see if this has any measurable performance impact.