-
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
SlotFill: use observableMap everywhere, remove manual rerendering #67400
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,31 +7,26 @@ import { useContext, useLayoutEffect, useRef } from '@wordpress/element'; | |
* Internal dependencies | ||
*/ | ||
import SlotFillContext from './context'; | ||
import useSlot from './use-slot'; | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The fact that there were two separate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The removed |
||
const instanceRef = useRef( {} ); | ||
const childrenRef = useRef( children ); | ||
|
||
const ref = useRef( { | ||
name, | ||
children, | ||
} ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here we used to store |
||
useLayoutEffect( () => { | ||
childrenRef.current = children; | ||
}, [ children ] ); | ||
|
||
useLayoutEffect( () => { | ||
const refValue = ref.current; | ||
refValue.name = name; | ||
registry.registerFill( name, refValue ); | ||
return () => registry.unregisterFill( name, refValue ); | ||
const instance = instanceRef.current; | ||
registry.registerFill( name, instance, childrenRef.current ); | ||
return () => registry.unregisterFill( name, instance ); | ||
}, [ registry, name ] ); | ||
|
||
useLayoutEffect( () => { | ||
ref.current.children = children; | ||
if ( slot ) { | ||
slot.rerender(); | ||
} | ||
}, [ slot, children ] ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code used to store the new value of |
||
registry.updateFill( name, instanceRef.current, childrenRef.current ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it still necessary to wrap the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the purpose of the It's an effect because it's a side effect. We want to "commit" the |
||
} ); | ||
|
||
return null; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,103 +8,102 @@ import { useState } from '@wordpress/element'; | |
*/ | ||
import SlotFillContext from './context'; | ||
import type { | ||
FillComponentProps, | ||
FillInstance, | ||
FillChildren, | ||
BaseSlotInstance, | ||
BaseSlotFillContext, | ||
SlotFillProviderProps, | ||
SlotKey, | ||
Rerenderable, | ||
} from './types'; | ||
import { observableMap } from '@wordpress/compose'; | ||
|
||
function createSlotRegistry(): BaseSlotFillContext { | ||
const slots: Record< SlotKey, Rerenderable > = {}; | ||
const fills: Record< SlotKey, FillComponentProps[] > = {}; | ||
let listeners: Array< () => void > = []; | ||
|
||
function registerSlot( name: SlotKey, slot: Rerenderable ) { | ||
const previousSlot = slots[ name ]; | ||
slots[ name ] = slot; | ||
triggerListeners(); | ||
|
||
// Sometimes the fills are registered after the initial render of slot | ||
// But before the registerSlot call, we need to rerender the slot. | ||
forceUpdateSlot( name ); | ||
|
||
// If a new instance of a slot is being mounted while another with the | ||
// same name exists, force its update _after_ the new slot has been | ||
// assigned into the instance, such that its own rendering of children | ||
// will be empty (the new Slot will subsume all fills for this name). | ||
if ( previousSlot ) { | ||
previousSlot.rerender(); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All this code collapses into a simple
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now when we use an observable store for The previous implementation was convoluted because:
|
||
|
||
function registerFill( name: SlotKey, instance: FillComponentProps ) { | ||
fills[ name ] = [ ...( fills[ name ] || [] ), instance ]; | ||
forceUpdateSlot( name ); | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Any reason why here (and in types.ts) we stopped using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I should give some nice name to the |
||
>(); | ||
|
||
function registerSlot( name: SlotKey, instance: BaseSlotInstance ) { | ||
slots.set( name, instance ); | ||
} | ||
|
||
function unregisterSlot( name: SlotKey, instance: Rerenderable ) { | ||
function unregisterSlot( name: SlotKey, instance: BaseSlotInstance ) { | ||
// If a previous instance of a Slot by this name unmounts, do nothing, | ||
// as the slot and its fills should only be removed for the current | ||
// known instance. | ||
if ( slots[ name ] !== instance ) { | ||
if ( slots.get( name ) !== instance ) { | ||
return; | ||
} | ||
|
||
delete slots[ name ]; | ||
triggerListeners(); | ||
slots.delete( name ); | ||
} | ||
|
||
function unregisterFill( name: SlotKey, instance: FillComponentProps ) { | ||
fills[ name ] = | ||
fills[ name ]?.filter( ( fill ) => fill !== instance ) ?? []; | ||
forceUpdateSlot( name ); | ||
function registerFill( | ||
name: SlotKey, | ||
instance: FillInstance, | ||
children: FillChildren | ||
) { | ||
fills.set( name, [ | ||
...( fills.get( name ) || [] ), | ||
{ instance, children }, | ||
] ); | ||
} | ||
|
||
function getSlot( name: SlotKey ): Rerenderable | undefined { | ||
return slots[ name ]; | ||
function unregisterFill( name: SlotKey, instance: FillInstance ) { | ||
const fillsForName = fills.get( name ); | ||
if ( ! fillsForName ) { | ||
return; | ||
} | ||
|
||
fills.set( | ||
name, | ||
fillsForName.filter( ( fill ) => fill.instance !== instance ) | ||
); | ||
} | ||
|
||
function getFills( | ||
function updateFill( | ||
name: SlotKey, | ||
slotInstance: Rerenderable | ||
): FillComponentProps[] { | ||
// 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 commentThe reason will be displayed to describe this comment to others. Learn more. This logic where the fills are |
||
instance: FillInstance, | ||
children: FillChildren | ||
) { | ||
const fillsForName = fills.get( name ); | ||
if ( ! fillsForName ) { | ||
return; | ||
} | ||
return fills[ name ]; | ||
} | ||
|
||
function forceUpdateSlot( name: SlotKey ) { | ||
const slot = getSlot( name ); | ||
|
||
if ( slot ) { | ||
slot.rerender(); | ||
const fillForInstance = fillsForName.find( | ||
( f ) => f.instance === instance | ||
); | ||
if ( ! fillForInstance ) { | ||
return; | ||
} | ||
} | ||
|
||
function triggerListeners() { | ||
listeners.forEach( ( listener ) => listener() ); | ||
} | ||
|
||
function subscribe( listener: () => void ) { | ||
listeners.push( listener ); | ||
if ( fillForInstance.children === children ) { | ||
return; | ||
} | ||
|
||
return () => { | ||
listeners = listeners.filter( ( l ) => l !== listener ); | ||
}; | ||
fills.set( | ||
name, | ||
fillsForName.map( ( f ) => { | ||
if ( f.instance === instance ) { | ||
// Replace with new record with updated `children`. | ||
return { instance, children }; | ||
} | ||
|
||
return f; | ||
} ) | ||
); | ||
} | ||
|
||
return { | ||
slots, | ||
fills, | ||
registerSlot, | ||
unregisterSlot, | ||
registerFill, | ||
unregisterFill, | ||
getSlot, | ||
getFills, | ||
subscribe, | ||
updateFill, | ||
}; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,10 +6,10 @@ import type { ReactElement, ReactNode, Key } from 'react'; | |
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useObservableValue } from '@wordpress/compose'; | ||
import { | ||
useContext, | ||
useEffect, | ||
useReducer, | ||
useRef, | ||
Children, | ||
cloneElement, | ||
|
@@ -32,41 +32,48 @@ function isFunction( maybeFunc: any ): maybeFunc is Function { | |
return typeof maybeFunc === 'function'; | ||
} | ||
|
||
function addKeysToChildren( children: ReactNode ) { | ||
return Children.map( children, ( child, childIndex ) => { | ||
if ( ! child || typeof child === 'string' ) { | ||
return child; | ||
} | ||
let childKey: Key = childIndex; | ||
if ( typeof child === 'object' && 'key' in child && child?.key ) { | ||
childKey = child.key; | ||
} | ||
|
||
return cloneElement( child as ReactElement, { | ||
key: childKey, | ||
} ); | ||
} ); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a simple refactor where the |
||
|
||
function Slot( props: Omit< SlotComponentProps, 'bubblesVirtually' > ) { | ||
const registry = useContext( SlotFillContext ); | ||
const [ , rerender ] = useReducer( () => [], [] ); | ||
const ref = useRef( { rerender } ); | ||
const instanceRef = useRef( {} ); | ||
|
||
const { name, children, fillProps = {} } = props; | ||
|
||
useEffect( () => { | ||
const refValue = ref.current; | ||
registry.registerSlot( name, refValue ); | ||
return () => registry.unregisterSlot( name, refValue ); | ||
const instance = instanceRef.current; | ||
registry.registerSlot( name, instance ); | ||
return () => registry.unregisterSlot( name, instance ); | ||
}, [ registry, name ] ); | ||
|
||
const fills: ReactNode[] = ( registry.getFills( name, ref.current ) ?? [] ) | ||
let fills = useObservableValue( registry.fills, name ) ?? []; | ||
const currentSlot = useObservableValue( registry.slots, name ); | ||
|
||
// Fills should only be rendered in the currently registered instance of the slot. | ||
if ( currentSlot !== instanceRef.current ) { | ||
fills = []; | ||
} | ||
|
||
const renderedFills = fills | ||
.map( ( fill ) => { | ||
const fillChildren = isFunction( fill.children ) | ||
? fill.children( fillProps ) | ||
: fill.children; | ||
return Children.map( fillChildren, ( child, childIndex ) => { | ||
if ( ! child || typeof child === 'string' ) { | ||
return child; | ||
} | ||
let childKey: Key = childIndex; | ||
if ( | ||
typeof child === 'object' && | ||
'key' in child && | ||
child?.key | ||
) { | ||
childKey = child.key; | ||
} | ||
|
||
return cloneElement( child as ReactElement, { | ||
key: childKey, | ||
} ); | ||
} ); | ||
return addKeysToChildren( fillChildren ); | ||
} ) | ||
.filter( | ||
// In some cases fills are rendered only when some conditions apply. | ||
|
@@ -75,7 +82,13 @@ function Slot( props: Omit< SlotComponentProps, 'bubblesVirtually' > ) { | |
( element ) => ! isEmptyElement( element ) | ||
); | ||
|
||
return <>{ isFunction( children ) ? children( fills ) : fills }</>; | ||
return ( | ||
<> | ||
{ isFunction( children ) | ||
? children( renderedFills ) | ||
: renderedFills } | ||
</> | ||
); | ||
} | ||
|
||
export default Slot; |
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
andgetFills
are replaced by simple map lookups:slots.get( name )
. Thesubscribe
method is also a method of the observable map now.