Skip to content
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

Data: withSelect: Subscribe only to reducer keys referenced in mapSelectToProps #12877

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 44 additions & 11 deletions packages/data/src/components/with-select/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,51 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped
/**
* Given a props object, returns the next merge props by mapSelectToProps.
*
* @param {Object} props Props to pass as argument to mapSelectToProps.
* @param {Function} select Selector getter function.
* @param {Object} ownProps The incoming props to the component.
* @param {WPDataRegistry} registry Contextual data registry.
*
* @return {Object} Props to merge into rendered wrapped element.
*/
function getNextMergeProps( props ) {
function getNextMergeProps( select, ownProps, registry ) {
return (
mapSelectToProps( props.registry.select, props.ownProps, props.registry ) ||
mapSelectToProps( select, ownProps, registry ) ||
DEFAULT_MERGE_PROPS
);
}

/**
* Given a select function, returns an enhanced function which returns the
* same result, but also observes the unique set of reducer keys with which
* the function has been called. The keys can be retrieved by calling the
* `getReducerKeys` function on the returned enhanced function.
*
* @param {Function} select Original select function.
*
* @return {Function} Enhanced select function.
*/
function createObservedSelect( select ) {
const reducerKeys = {};

function observedSelect( reducerKey ) {
reducerKeys[ reducerKey ] = true;
return select( reducerKey );
}

observedSelect.getReducerKeys = function() {
return Object.keys( reducerKeys );
};

return observedSelect;
}

class ComponentWithSelect extends Component {
constructor( props ) {
super( props );

this.onStoreChange = this.onStoreChange.bind( this );

this.subscribe( props.registry );

this.mergeProps = getNextMergeProps( props );
this.subscribe( props );
}

componentDidMount() {
Expand All @@ -77,7 +102,7 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped
const hasRegistryChanged = nextProps.registry !== this.props.registry;
if ( hasRegistryChanged ) {
this.unsubscribe();
this.subscribe( nextProps.registry );
this.subscribe( nextProps );
}

// Treat a registry change as equivalent to `ownProps`, to reflect
Expand All @@ -94,7 +119,8 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped
}

if ( hasPropsChanged ) {
const nextMergeProps = getNextMergeProps( nextProps );
const { registry, ownProps } = nextProps;
const nextMergeProps = getNextMergeProps( registry.select, ownProps, registry );
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.
Expand All @@ -119,7 +145,8 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped
return;
}

const nextMergeProps = getNextMergeProps( this.props );
const { registry, ownProps } = this.props;
const nextMergeProps = getNextMergeProps( registry.select, ownProps, registry );
if ( isShallowEqual( this.mergeProps, nextMergeProps ) ) {
return;
}
Expand All @@ -136,8 +163,14 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped
this.setState( {} );
}

subscribe( registry ) {
this.unsubscribe = registry.subscribe( this.onStoreChange );
subscribe( props ) {
const { registry, ownProps } = props;

const observedSelect = createObservedSelect( registry.select );

this.mergeProps = getNextMergeProps( observedSelect, ownProps, registry );

this.unsubscribe = registry.subscribe( this.onStoreChange, observedSelect.getReducerKeys() );
}

render() {
Expand Down
2 changes: 1 addition & 1 deletion packages/data/src/namespace-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default function createNamespace( key, options, registry ) {
lastState = state;

if ( hasChanged ) {
listener();
listener( key );
}
} );
};
Expand Down
74 changes: 65 additions & 9 deletions packages/data/src/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import {
castArray,
without,
mapValues,
} from 'lodash';
Expand All @@ -12,6 +13,16 @@ import {
import createNamespace from './namespace-store.js';
import dataStore from './store';

/**
* Given an array of functions, invokes each function in the array with an
* empty argument set.
*
* @param {Function[]} fns Functions to invoke.
*/
function invokeForEach( fns ) {
fns.forEach( ( fn ) => fn() );
}

/**
* An isolated orchestrator of store registrations.
*
Expand Down Expand Up @@ -40,27 +51,72 @@ import dataStore from './store';
*/
export function createRegistry( storeConfigs = {} ) {
const stores = {};
let listeners = [];
let globalListeners = [];
const listenersByKey = {};

/**
* Global listener called for each store's update.
*
* @param {?string} reducerKey Key of reducer which changed, if provided by
* the registry implementation.
*/
function globalListener() {
listeners.forEach( ( listener ) => listener() );
function onStoreChange( reducerKey ) {
invokeForEach( globalListeners );

if ( reducerKey ) {
if ( listenersByKey[ reducerKey ] ) {
invokeForEach( listenersByKey[ reducerKey ] );
}
} else {
// For backwards compatibility with non-namespace-store, reducerKey
// is optional. If omitted, call every listenersByKey.
for ( const [ , listeners ] of Object.entries( listenersByKey ) ) {
invokeForEach( listeners );
}
}
}

/**
* Subscribe to changes to any data.
*
* @param {Function} listener Listener function.
* @param {Function} listener Listener function.
* @param {(string|Array<string>)} reducerKeys Subset of reducer keys on
* which subscribe function
* should be called.
*
* @return {Function} Unsubscribe function.
* @return {Function} Unsubscribe function.
*/
const subscribe = ( listener ) => {
listeners.push( listener );
const subscribe = ( listener, reducerKeys ) => {
if ( reducerKeys ) {
// Overload to support string argument of `reducerKeys`.
reducerKeys = castArray( reducerKeys );

reducerKeys.forEach( ( reducerKey ) => {
if ( ! listenersByKey[ reducerKey ] ) {
listenersByKey[ reducerKey ] = [];
}

listenersByKey[ reducerKey ].push( listener );
} );

return () => {
reducerKeys.forEach( ( reducerKey ) => {
listenersByKey[ reducerKey ] = without(
listenersByKey[ reducerKey ],
listener
);

if ( ! listenersByKey[ reducerKey ].length ) {
delete listenersByKey[ reducerKey ];
}
} );
};
}

globalListeners.push( listener );

return () => {
listeners = without( listeners, listener );
globalListeners = without( globalListeners, listener );
};
};

Expand Down Expand Up @@ -122,7 +178,7 @@ export function createRegistry( storeConfigs = {} ) {
throw new TypeError( 'config.subscribe must be a function' );
}
stores[ key ] = config;
config.subscribe( globalListener );
config.subscribe( onStoreChange );
}

let registry = {
Expand Down