Skip to content

Commit

Permalink
SlotFill: rewrite base Slot to functional, unify rerenderable refs (#…
Browse files Browse the repository at this point in the history
…67153)

* SlotFill: rewrite base Slot to functional, unify rerenderable refs

* Add changelog entry

* Implement rerender with useReducer

Co-authored-by: jsnajdr <[email protected]>
Co-authored-by: Mamaduka <[email protected]>
Co-authored-by: tyxla <[email protected]>
  • Loading branch information
4 people authored Nov 20, 2024
1 parent ebf1149 commit 944e6b8
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 181 deletions.
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
### Internal

- `SlotFill`: fix dependencies of `Fill` registration effects ([#67071](https://github.com/WordPress/gutenberg/pull/67071)).
- `SlotFill`: rewrite the `Slot` component from class component to functional ([#67153](https://github.com/WordPress/gutenberg/pull/67153)).

## 28.12.0 (2024-11-16)

Expand Down
27 changes: 5 additions & 22 deletions packages/components/src/slot-fill/bubbles-virtually/fill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import { useObservableValue } from '@wordpress/compose';
import {
useContext,
useReducer,
useRef,
useState,
useEffect,
createPortal,
} from '@wordpress/element';
Expand All @@ -17,37 +17,20 @@ import SlotFillContext from './slot-fill-context';
import StyleProvider from '../../style-provider';
import type { FillComponentProps } from '../types';

function useForceUpdate() {
const [ , setState ] = useState( {} );
const mountedRef = useRef( true );

useEffect( () => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, [] );

return () => {
if ( mountedRef.current ) {
setState( {} );
}
};
}

export default function Fill( { name, children }: FillComponentProps ) {
const registry = useContext( SlotFillContext );
const slot = useObservableValue( registry.slots, name );
const rerender = useForceUpdate();
const [ , rerender ] = useReducer( () => [], [] );
const ref = useRef( { rerender } );

useEffect( () => {
// We register fills so we can keep track of their existence.
// Some Slot implementations need to know if there're already fills
// registered so they can choose to render themselves or not.
registry.registerFill( name, ref );
const refValue = ref.current;
registry.registerFill( name, refValue );
return () => {
registry.unregisterFill( name, ref );
registry.unregisterFill( name, refValue );
};
}, [ registry, name ] );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function createSlotRegistry(): SlotFillBubblesVirtuallyContext {
const slotFills = fills.get( name );
if ( slotFills ) {
// Force update fills.
slotFills.forEach( ( fill ) => fill.current.rerender() );
slotFills.forEach( ( fill ) => fill.rerender() );
}
};

Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/slot-fill/fill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function Fill( { name, children }: FillComponentProps ) {
useLayoutEffect( () => {
ref.current.children = children;
if ( slot ) {
slot.forceUpdate();
slot.rerender();
}
}, [ slot, children ] );

Expand Down
25 changes: 8 additions & 17 deletions packages/components/src/slot-fill/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* WordPress dependencies
*/
import type { Component } from '@wordpress/element';
import { useState } from '@wordpress/element';

/**
Expand All @@ -11,20 +10,17 @@ import SlotFillContext from './context';
import type {
FillComponentProps,
BaseSlotFillContext,
BaseSlotComponentProps,
SlotFillProviderProps,
SlotKey,
Rerenderable,
} from './types';

function createSlotRegistry(): BaseSlotFillContext {
const slots: Record< SlotKey, Component< BaseSlotComponentProps > > = {};
const slots: Record< SlotKey, Rerenderable > = {};
const fills: Record< SlotKey, FillComponentProps[] > = {};
let listeners: Array< () => void > = [];

function registerSlot(
name: SlotKey,
slot: Component< BaseSlotComponentProps >
) {
function registerSlot( name: SlotKey, slot: Rerenderable ) {
const previousSlot = slots[ name ];
slots[ name ] = slot;
triggerListeners();
Expand All @@ -38,7 +34,7 @@ function createSlotRegistry(): BaseSlotFillContext {
// 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.forceUpdate();
previousSlot.rerender();
}
}

Expand All @@ -47,10 +43,7 @@ function createSlotRegistry(): BaseSlotFillContext {
forceUpdateSlot( name );
}

function unregisterSlot(
name: SlotKey,
instance: Component< BaseSlotComponentProps >
) {
function unregisterSlot( name: SlotKey, instance: Rerenderable ) {
// 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.
Expand All @@ -68,15 +61,13 @@ function createSlotRegistry(): BaseSlotFillContext {
forceUpdateSlot( name );
}

function getSlot(
name: SlotKey
): Component< BaseSlotComponentProps > | undefined {
function getSlot( name: SlotKey ): Rerenderable | undefined {
return slots[ name ];
}

function getFills(
name: SlotKey,
slotInstance: Component< BaseSlotComponentProps >
slotInstance: Rerenderable
): FillComponentProps[] {
// Fills should only be returned for the current instance of the slot
// in which they occupy.
Expand All @@ -90,7 +81,7 @@ function createSlotRegistry(): BaseSlotFillContext {
const slot = getSlot( name );

if ( slot ) {
slot.forceUpdate();
slot.rerender();
}
}

Expand Down
133 changes: 48 additions & 85 deletions packages/components/src/slot-fill/slot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import type { ReactElement, ReactNode, Key } from 'react';
* WordPress dependencies
*/
import {
useContext,
useEffect,
useReducer,
useRef,
Children,
Component,
cloneElement,
isEmptyElement,
} from '@wordpress/element';
Expand All @@ -17,7 +20,7 @@ import {
* Internal dependencies
*/
import SlotFillContext from './context';
import type { BaseSlotComponentProps, SlotComponentProps } from './types';
import type { SlotComponentProps } from './types';

/**
* Whether the argument is a function.
Expand All @@ -29,90 +32,50 @@ function isFunction( maybeFunc: any ): maybeFunc is Function {
return typeof maybeFunc === 'function';
}

class SlotComponent extends Component< BaseSlotComponentProps > {
private isUnmounted: boolean;

constructor( props: BaseSlotComponentProps ) {
super( props );

this.isUnmounted = false;
}

componentDidMount() {
const { registerSlot } = this.props;
this.isUnmounted = false;
registerSlot( this.props.name, this );
}

componentWillUnmount() {
const { unregisterSlot } = this.props;
this.isUnmounted = true;
unregisterSlot( this.props.name, this );
}

componentDidUpdate( prevProps: BaseSlotComponentProps ) {
const { name, unregisterSlot, registerSlot } = this.props;

if ( prevProps.name !== name ) {
unregisterSlot( prevProps.name, this );
registerSlot( name, this );
}
}

forceUpdate() {
if ( this.isUnmounted ) {
return;
}
super.forceUpdate();
}

render() {
const { children, name, fillProps = {}, getFills } = this.props;
const fills: ReactNode[] = ( getFills( name, this ) ?? [] )
.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,
} );
function Slot( props: Omit< SlotComponentProps, 'bubblesVirtually' > ) {
const registry = useContext( SlotFillContext );
const [ , rerender ] = useReducer( () => [], [] );
const ref = useRef( { rerender } );

const { name, children, fillProps = {} } = props;

useEffect( () => {
const refValue = ref.current;
registry.registerSlot( name, refValue );
return () => registry.unregisterSlot( name, refValue );
}, [ registry, name ] );

const fills: ReactNode[] = ( registry.getFills( name, ref.current ) ?? [] )
.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,
} );
} )
.filter(
// In some cases fills are rendered only when some conditions apply.
// This ensures that we only use non-empty fills when rendering, i.e.,
// it allows us to render wrappers only when the fills are actually present.
( element ) => ! isEmptyElement( element )
);

return <>{ isFunction( children ) ? children( fills ) : fills }</>;
}
} );
} )
.filter(
// In some cases fills are rendered only when some conditions apply.
// This ensures that we only use non-empty fills when rendering, i.e.,
// it allows us to render wrappers only when the fills are actually present.
( element ) => ! isEmptyElement( element )
);

return <>{ isFunction( children ) ? children( fills ) : fills }</>;
}

const Slot = ( props: Omit< SlotComponentProps, 'bubblesVirtually' > ) => (
<SlotFillContext.Consumer>
{ ( { registerSlot, unregisterSlot, getFills } ) => (
<SlotComponent
{ ...props }
registerSlot={ registerSlot }
unregisterSlot={ unregisterSlot }
getFills={ getFills }
/>
) }
</SlotFillContext.Consumer>
);

export default Slot;
Loading

0 comments on commit 944e6b8

Please sign in to comment.