194 lines
6.3 KiB
JavaScript
194 lines
6.3 KiB
JavaScript
/**
|
|
* NOTE: This is temporary code. It exists only until a version of `@wordpress/data`
|
|
* is released which supports this functionality.
|
|
*
|
|
* TODO: Remove this and use `@wordpress/data` `withSelect` instead after
|
|
* this PR is merged: https://github.com/WordPress/gutenberg/pull/11460
|
|
*
|
|
* @format
|
|
*/
|
|
|
|
/**
|
|
* External dependencies
|
|
*/
|
|
import { isFunction } from 'lodash';
|
|
import { Component } from '@wordpress/element';
|
|
import isShallowEqual from '@wordpress/is-shallow-equal';
|
|
import { createHigherOrderComponent } from '@wordpress/compose';
|
|
import { RegistryConsumer } from '@wordpress/data';
|
|
|
|
/**
|
|
* Higher-order component used to inject state-derived props using registered
|
|
* selectors.
|
|
*
|
|
* @param {Function} mapSelectToProps Function called on every state change,
|
|
* expected to return object of props to
|
|
* merge with the component's own props.
|
|
*
|
|
* @return {Component} Enhanced component with merged state data props.
|
|
*/
|
|
const withSelect = mapSelectToProps =>
|
|
createHigherOrderComponent( WrappedComponent => {
|
|
/**
|
|
* Default merge props. A constant value is used as the fallback since it
|
|
* can be more efficiently shallow compared in case component is repeatedly
|
|
* rendered without its own merge props.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
const DEFAULT_MERGE_PROPS = {};
|
|
|
|
class ComponentWithSelect extends Component {
|
|
constructor( props ) {
|
|
super( props );
|
|
|
|
this.onStoreChange = this.onStoreChange.bind( this );
|
|
this.subscribe( props.registry );
|
|
|
|
this.onUnmounts = {};
|
|
this.mergeProps = this.getNextMergeProps( props );
|
|
}
|
|
|
|
/**
|
|
* Given a props object, returns the next merge props by mapSelectToProps.
|
|
*
|
|
* @param {Object} props Props to pass as argument to mapSelectToProps.
|
|
*
|
|
* @return {Object} Props to merge into rendered wrapped element.
|
|
*/
|
|
getNextMergeProps( props ) {
|
|
const storeSelectors = {};
|
|
const onCompletes = [];
|
|
const onUnmounts = {};
|
|
const componentContext = { component: this };
|
|
|
|
const getStoreFromRegistry = ( key, registry, context ) => {
|
|
// This is our first time selecting from this store.
|
|
// Do some lazy-loading of handling at this time.
|
|
const selectorsForKey = registry.select( key );
|
|
|
|
if ( isFunction( selectorsForKey ) ) {
|
|
// This store has special handling for its selectors.
|
|
// We give it a context, and we check for a "resolve"
|
|
const { selectors, onComplete, onUnmount } = selectorsForKey( context );
|
|
onComplete && onCompletes.push( onComplete );
|
|
onUnmount && ( onUnmounts[ key ] = onUnmount );
|
|
storeSelectors[ key ] = selectors;
|
|
} else {
|
|
storeSelectors[ key ] = selectorsForKey;
|
|
}
|
|
};
|
|
|
|
const select = key => {
|
|
if ( ! storeSelectors[ key ] ) {
|
|
getStoreFromRegistry( key, props.registry, componentContext );
|
|
}
|
|
|
|
return storeSelectors[ key ];
|
|
};
|
|
|
|
const selectedProps = mapSelectToProps( select, props.ownProps ) || DEFAULT_MERGE_PROPS;
|
|
|
|
// Complete the select for those stores which support it.
|
|
onCompletes.forEach( onComplete => onComplete() );
|
|
return selectedProps;
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.canRunSelection = true;
|
|
|
|
// A state change may have occurred between the constructor and
|
|
// mount of the component (e.g. during the wrapped component's own
|
|
// constructor), in which case selection should be rerun.
|
|
if ( this.hasQueuedSelection ) {
|
|
this.hasQueuedSelection = false;
|
|
this.onStoreChange();
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.canRunSelection = false;
|
|
this.unsubscribe();
|
|
Object.keys( this.onUnmounts ).forEach( key => this.onUnmounts[ key ]() );
|
|
}
|
|
|
|
shouldComponentUpdate( nextProps, nextState ) {
|
|
// Cycle subscription if registry changes.
|
|
const hasRegistryChanged = nextProps.registry !== this.props.registry;
|
|
if ( hasRegistryChanged ) {
|
|
this.unsubscribe();
|
|
this.subscribe( nextProps.registry );
|
|
}
|
|
|
|
// Treat a registry change as equivalent to `ownProps`, to reflect
|
|
// `mergeProps` to rendered component if and only if updated.
|
|
const hasPropsChanged =
|
|
hasRegistryChanged || ! isShallowEqual( this.props.ownProps, nextProps.ownProps );
|
|
|
|
// Only render if props have changed or merge props have been updated
|
|
// from the store subscriber.
|
|
if ( this.state === nextState && ! hasPropsChanged ) {
|
|
return false;
|
|
}
|
|
|
|
if ( hasPropsChanged ) {
|
|
const nextMergeProps = this.getNextMergeProps( nextProps );
|
|
if ( ! isShallowEqual( this.mergeProps, nextMergeProps ) ) {
|
|
// If merge props change as a result of the incoming props,
|
|
// they should be reflected as such in the upcoming render.
|
|
// While side effects are discouraged in lifecycle methods,
|
|
// this component is used heavily, and prior efforts to use
|
|
// `getDerivedStateFromProps` had demonstrated miserable
|
|
// performance.
|
|
this.mergeProps = nextMergeProps;
|
|
}
|
|
|
|
// Regardless whether merge props are changing, fall through to
|
|
// incur the render since the component will need to receive
|
|
// the changed `ownProps`.
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
onStoreChange() {
|
|
if ( ! this.canRunSelection ) {
|
|
this.hasQueuedSelection = true;
|
|
return;
|
|
}
|
|
|
|
const nextMergeProps = this.getNextMergeProps( this.props );
|
|
if ( isShallowEqual( this.mergeProps, nextMergeProps ) ) {
|
|
return;
|
|
}
|
|
|
|
this.mergeProps = nextMergeProps;
|
|
|
|
// Schedule an update. Merge props are not assigned to state since
|
|
// derivation of merge props from incoming props occurs within
|
|
// shouldComponentUpdate, where setState is not allowed. setState
|
|
// is used here instead of forceUpdate because forceUpdate bypasses
|
|
// shouldComponentUpdate altogether, which isn't desireable if both
|
|
// state and props change within the same render. Unfortunately,
|
|
// this requires that next merge props are generated twice.
|
|
this.setState( {} );
|
|
}
|
|
|
|
subscribe( registry ) {
|
|
this.unsubscribe = registry.subscribe( this.onStoreChange );
|
|
}
|
|
|
|
render() {
|
|
return <WrappedComponent { ...this.props.ownProps } { ...this.mergeProps } />;
|
|
}
|
|
}
|
|
|
|
return ownProps => (
|
|
<RegistryConsumer>
|
|
{ registry => <ComponentWithSelect ownProps={ ownProps } registry={ registry } /> }
|
|
</RegistryConsumer>
|
|
);
|
|
}, 'withSelect' );
|
|
|
|
export default withSelect;
|