-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
SlotFill: use observableMap everywhere, remove manual rerendering #67400
base: trunk
Are you sure you want to change the base?
Conversation
56eb04b
to
3956f58
Compare
Flaky tests detected in 2f69488. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/12123240256
|
2f69488
to
c0cae70
Compare
c0cae70
to
f2321f4
Compare
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. |
registerSlot: () => {}, | ||
unregisterSlot: () => {}, | ||
registerFill: () => {}, | ||
unregisterFill: () => {}, | ||
getSlot: () => undefined, | ||
getFills: () => [], | ||
subscribe: () => () => {}, |
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.
getSlot
and getFills
are replaced by simple map lookups: slots.get( name )
. The subscribe
method is also a method of the observable map now.
import type { FillComponentProps } from './types'; | ||
|
||
export default function Fill( { name, children }: FillComponentProps ) { | ||
const registry = useContext( SlotFillContext ); | ||
const slot = useSlot( name ); |
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 Fill
no longer needs the slot
value because it no longer calls slot.rerender()
. Instead, it updates the value in the fills
map (updateFill
call) and the relevant slot rerenders automatically because it listens for fills
changes.
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 fact that there were two separate useSlot
hooks was definitely confusing. Glad to see that the "non-bubblesVirtually" version is going away
const ref = useRef( { | ||
name, | ||
children, | ||
} ); |
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.
Here we used to store name
in the fills
map but that was redundant. name
is a key in the map. It doesn't need to be stored also in the value.
if ( slot ) { | ||
slot.rerender(); | ||
} | ||
}, [ slot, children ] ); |
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 code used to store the new value of children
into a ref.current
. But that's a "silent" mutation that doesn't trigger any listeners. That's why the code had to do a manual slot.rerender()
call. The new code updates the children
using the observableMap.set
method (hidden inside the updateFill
method). Because the slot
listens for fills updates via useObservableValue( fills, name )
, it gets notified and rerenders automatically.
if ( previousSlot ) { | ||
previousSlot.rerender(); | ||
} | ||
} |
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.
All this code collapses into a simple slots.set
call:
triggerListeners
is built-in inslots.set
.- the
forceUpdateSlot
logic is built-in intouseSyncExternalStore
. It handles the situation when the store is updated after the consumer component is rendered and before it subscribes to updates in an effect. previousSlot.rerender()
happens automatically because the old slot calls thecurrentSlot = useObservableValue( slots, name )
hook and gets notified when a new slot is registered under the same name.
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 love how elegant this is ! I wonder if we should document it somewhere, though, since otherwise there would be a lot of implicit knowledge required to fully understand the component.
// Fills should only be returned for the current instance of the slot | ||
// in which they occupy. | ||
if ( slots[ name ] !== slotInstance ) { | ||
return []; |
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 logic where the fills are []
for a slot instance that has been overwritten in the store by a new instance, it has moved into the Slot
component.
key: childKey, | ||
} ); | ||
} ); | ||
} |
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 a simple refactor where the addKeysToChildren
function has been extracted from the big Slot
component. Without any change.
@@ -84,6 +84,10 @@ export type SlotComponentProps = | |||
style?: never; | |||
} ); | |||
|
|||
export type FillChildren = | |||
| ReactNode | |||
| ( ( fillProps: FillProps ) => ReactNode ); |
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.
Extracted this as a separate type because it's used in the fills
map.
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.
Thank you for working on this! I find it exciting to see how, little by little, SlotFill code is getting simpler, and the two implementations are getting closer to each other — I'm hoping we will be able to merge them into one single implementation at some point.
Apart from smoke testing Storybook and the editor, I found it hard to test the changes. I wonder if, as a follow-up, we should also spend some time to bolster the suite of unit tests (which is weirdly still written in JS and not in TS).
slot.rerender(); | ||
} | ||
}, [ slot, children ] ); | ||
registry.updateFill( name, instanceRef.current, childrenRef.current ); |
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 it still necessary to wrap the updateFill
call inside a useLayoutEffect
, especially given that the hook doesn't have a dependency list anymore?
if ( previousSlot ) { | ||
previousSlot.rerender(); | ||
} | ||
} |
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 love how elegant this is ! I wonder if we should document it somewhere, though, since otherwise there would be a lot of implicit knowledge required to fully understand the component.
const slots = observableMap< SlotKey, BaseSlotInstance >(); | ||
const fills = observableMap< | ||
SlotKey, | ||
{ instance: FillInstance; children: FillChildren }[] |
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 why here (and in types.ts) we stopped using FillComponentProps
?
I think that would be wonderful. A good unit test coverage for SlotFills is crucial. |
Rewrites the base (
bubblesVirtually=false
, non-portal) version ofSlotFill
to useobservableMap
for storing slots and fills. This removes the need for all manual rerenders. The observable maps will notify their consumers automatically whenever something interesting and rerender-worthy happens.With this PR, both variants of
SlotFill
become implemented in a very similar way.There are observable maps for slots and fills. Each is registering the same base info (instance id) and some additional implementation-specific data:
children
that the slot should renderfillProps
that the fill uses during rendering, and the DOM element into which the fill is renderedThe context has methods for registering/unregistering slots and fills, and additional methods:
updateFill
method that updates thechildren
in registry onFill
rerenderupdateSlot
method that updates the element/fillProps
in registry onSlot
rerender