From 32068e8b36cb7a23243829d5ef608deee70f2d13 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 3 Jan 2019 10:55:37 -0500 Subject: [PATCH 1/5] Data: Support subscribers per reducer key(s) --- packages/data/src/namespace-store.js | 2 +- packages/data/src/registry.js | 74 ++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/packages/data/src/namespace-store.js b/packages/data/src/namespace-store.js index dc0c0075fd620..1f2dceb74201d 100644 --- a/packages/data/src/namespace-store.js +++ b/packages/data/src/namespace-store.js @@ -54,7 +54,7 @@ export default function createNamespace( key, options, registry ) { lastState = state; if ( hasChanged ) { - listener(); + listener( key ); } } ); }; diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index acca73b93129a..351fe72bfcc12 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -2,6 +2,7 @@ * External dependencies */ import { + castArray, without, mapValues, } from 'lodash'; @@ -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. * @@ -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)} reducerKeys Optional 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 ); }; }; @@ -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 = { From d687e3345e99c13985c65854c88eb20c5617e2b3 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 3 Jan 2019 10:56:02 -0500 Subject: [PATCH 2/5] Data: withSelect: Pass through reducerKeys hint if provided --- packages/data/src/components/with-select/index.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/data/src/components/with-select/index.js b/packages/data/src/components/with-select/index.js index d1219479d3e46..b2a9b22e2939a 100644 --- a/packages/data/src/components/with-select/index.js +++ b/packages/data/src/components/with-select/index.js @@ -14,13 +14,18 @@ import { RegistryConsumer } from '../registry-provider'; * 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. + * @param {Function} mapSelectToProps Function called on every + * state change, expected to + * return object of props to + * merge with the component's + * own props. + * @param {?(string|Array)} reducerKeys Optional subset of reducer + * keys on which subscribe + * callback should be called. * * @return {Component} Enhanced component with merged state data props. */ -const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( WrappedComponent ) => { +const withSelect = ( mapSelectToProps, reducerKeys ) => 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 @@ -137,7 +142,7 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped } subscribe( registry ) { - this.unsubscribe = registry.subscribe( this.onStoreChange ); + this.unsubscribe = registry.subscribe( this.onStoreChange, reducerKeys ); } render() { From 1cda41edf1cecda8ebaaf7af813e692103ac35b8 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 3 Jan 2019 12:12:26 -0500 Subject: [PATCH 3/5] Packages: Add package babel-plugin-transform-with-select --- .../package.json | 35 +++++++++++ .../src/index.js | 58 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 packages/babel-plugin-transform-with-select/package.json create mode 100644 packages/babel-plugin-transform-with-select/src/index.js diff --git a/packages/babel-plugin-transform-with-select/package.json b/packages/babel-plugin-transform-with-select/package.json new file mode 100644 index 0000000000000..cb099c11cf06d --- /dev/null +++ b/packages/babel-plugin-transform-with-select/package.json @@ -0,0 +1,35 @@ +{ + "name": "@wordpress/babel-plugin-transform-with-select", + "version": "1.0.0", + "description": "WordPress Babel transform to provide withSelect reducerKeys hint.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "babel", + "plugin" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/babel-plugin-transform-with-select/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "files": [ + "build", + "build-module" + ], + "main": "build/index.js", + "module": "build-module/index.js", + "dependencies": { + "@babel/runtime": "^7.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/babel-plugin-transform-with-select/src/index.js b/packages/babel-plugin-transform-with-select/src/index.js new file mode 100644 index 0000000000000..15c555c175f1b --- /dev/null +++ b/packages/babel-plugin-transform-with-select/src/index.js @@ -0,0 +1,58 @@ +export default function( babel ) { + const { types: t } = babel; + + function addNamespaceToWithSelect( path, namespace ) { + let parentPath = path; + while ( ( parentPath = parentPath.parentPath ) ) { + const { node } = parentPath; + if ( node.type !== 'CallExpression' || node.callee.name !== 'withSelect' ) { + continue; + } + + let reducerKeys; + if ( node.arguments.length > 1 ) { + const reducerKeysNode = node.arguments[ 1 ]; + switch ( reducerKeysNode.type ) { + case 'ArrayExpression': + reducerKeys = reducerKeysNode.elements.reduce( ( result, element ) => { + if ( element.type === 'StringLiteral' ) { + result.push( element.value ); + } + + return result; + }, [] ); + break; + case 'StringLiteral': + reducerKeys = [ reducerKeysNode.value ]; + break; + } + + if ( reducerKeys.includes( namespace ) ) { + break; + } + + reducerKeys.push( namespace ); + reducerKeys = reducerKeys.map( ( key ) => t.stringLiteral( key ) ); + const argumentPaths = parentPath.get( 'arguments' ); + argumentPaths[ 1 ].replaceWith( t.arrayExpression( reducerKeys ) ); + } else { + parentPath.pushContainer( 'arguments', t.stringLiteral( namespace ) ); + } + + break; + } + } + + return { + visitor: { + CallExpression: { + exit( path ) { + const { node } = path; + if ( node.callee.name === 'select' && node.arguments.length > 0 && node.arguments[ 0 ].type === 'StringLiteral' ) { + addNamespaceToWithSelect( path, node.arguments[ 0 ].value ); + } + }, + }, + }, + }; +} From 27f39ffd1022d87580a977641249e6f2660cbc36 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 3 Jan 2019 12:12:45 -0500 Subject: [PATCH 4/5] Build Tools: Include babel-plugin-transform-with-select in build --- babel.config.js | 1 + package-lock.json | 7 +++++++ package.json | 1 + 3 files changed, 9 insertions(+) diff --git a/babel.config.js b/babel.config.js index 6a903eff6c1d9..b4e3b3777debb 100644 --- a/babel.config.js +++ b/babel.config.js @@ -4,6 +4,7 @@ module.exports = function( api ) { return { presets: [ '@wordpress/babel-preset-default' ], plugins: [ + '@wordpress/babel-plugin-transform-with-select', [ '@wordpress/babel-plugin-import-jsx-pragma', { diff --git a/package-lock.json b/package-lock.json index 52818081f01b4..ecf41ead2d3e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2319,6 +2319,13 @@ "lodash": "^4.17.10" } }, + "@wordpress/babel-plugin-transform-with-select": { + "version": "file:packages/babel-plugin-transform-with-select", + "dev": true, + "requires": { + "@babel/runtime": "^7.0.0" + } + }, "@wordpress/babel-preset-default": { "version": "file:packages/babel-preset-default", "dev": true, diff --git a/package.json b/package.json index 95ce65ad2682f..25fcf72b93420 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@babel/traverse": "7.0.0", "@wordpress/babel-plugin-import-jsx-pragma": "file:packages/babel-plugin-import-jsx-pragma", "@wordpress/babel-plugin-makepot": "file:packages/babel-plugin-makepot", + "@wordpress/babel-plugin-transform-with-select": "file:packages/babel-plugin-transform-with-select", "@wordpress/babel-preset-default": "file:packages/babel-preset-default", "@wordpress/browserslist-config": "file:packages/browserslist-config", "@wordpress/custom-templated-path-webpack-plugin": "file:packages/custom-templated-path-webpack-plugin", From 7e5b0e2af33ea2062aa41698d8c03bbf4777113b Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 3 Jan 2019 12:27:40 -0500 Subject: [PATCH 5/5] babel-plugin-transform-with-select: Avoid unnecessary exit condition Originally included as part of early implementation where namespaces determined by inner nodes from exit of withSelect call expression --- .../babel-plugin-transform-with-select/src/index.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/babel-plugin-transform-with-select/src/index.js b/packages/babel-plugin-transform-with-select/src/index.js index 15c555c175f1b..b4d8ebc69e54e 100644 --- a/packages/babel-plugin-transform-with-select/src/index.js +++ b/packages/babel-plugin-transform-with-select/src/index.js @@ -45,13 +45,11 @@ export default function( babel ) { return { visitor: { - CallExpression: { - exit( path ) { - const { node } = path; - if ( node.callee.name === 'select' && node.arguments.length > 0 && node.arguments[ 0 ].type === 'StringLiteral' ) { - addNamespaceToWithSelect( path, node.arguments[ 0 ].value ); - } - }, + CallExpression( path ) { + const { node } = path; + if ( node.callee.name === 'select' && node.arguments.length > 0 && node.arguments[ 0 ].type === 'StringLiteral' ) { + addNamespaceToWithSelect( path, node.arguments[ 0 ].value ); + } }, }, };