From 9b136bbc95815ff2bfa2a96a4af258b402d715d8 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Wed, 7 Dec 2022 09:32:30 +0100 Subject: [PATCH] Add ability to subscribe to one store, remove __unstableSubscribeStore (#45513) * Add ability to subscribe to one store, remove __unstableSubscribeStore * Check typeof string in getStoreName() --- packages/data/CHANGELOG.md | 4 + packages/data/README.md | 8 +- .../data/src/components/use-select/index.js | 4 +- packages/data/src/index.js | 10 ++- packages/data/src/registry.js | 85 ++++++++----------- packages/data/src/test/registry.js | 7 +- 6 files changed, 56 insertions(+), 62 deletions(-) diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md index 380537c325ecaf..427699ad83f749 100644 --- a/packages/data/CHANGELOG.md +++ b/packages/data/CHANGELOG.md @@ -6,6 +6,10 @@ - Updated dependencies to require React 18 ([45235](https://github.com/WordPress/gutenberg/pull/45235)) +### Enhancements + +- The `registry.subscribe` function can now subscribe to updates only from one specific store, with a new optional parameter. + ## 7.6.0 (2022-11-16) ## 7.5.0 (2022-11-02) diff --git a/packages/data/README.md b/packages/data/README.md index d45a71d3fa6031..45f5054ef052e5 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -658,8 +658,11 @@ _Returns_ ### subscribe Given a listener function, the function will be called any time the state value -of one of the registered stores has changed. This function returns a `unsubscribe` -function used to stop the subscription. +of one of the registered stores has changed. If you specify the optional +`storeNameOrDescriptor` parameter, the listener function will be called only +on updates on that one specific registered store. + +This function returns an `unsubscribe` function used to stop the subscription. _Usage_ @@ -678,6 +681,7 @@ unsubscribe(); _Parameters_ - _listener_ `Function`: Callback function. +- _storeNameOrDescriptor_ `string|StoreDescriptor?`: Optional store name. ### suspendSelect diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index bc9d2f71bdedda..0be26286eb6c6e 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -242,7 +242,7 @@ export default function useSelect( mapSelect, deps ) { onStoreChange(); const unsubscribers = listeningStores.current.map( ( storeName ) => - registry.__unstableSubscribeStore( storeName, onChange ) + registry.subscribe( onChange, storeName ) ); isMounted.current = true; @@ -374,7 +374,7 @@ export function useSuspenseSelect( mapSelect, deps ) { onStoreChange(); const unsubscribers = listeningStores.current.map( ( storeName ) => - registry.__unstableSubscribeStore( storeName, onChange ) + registry.subscribe( onChange, storeName ) ); isMounted.current = true; diff --git a/packages/data/src/index.js b/packages/data/src/index.js index 91675d48c0354d..735ca5111e8baa 100644 --- a/packages/data/src/index.js +++ b/packages/data/src/index.js @@ -159,10 +159,14 @@ export const dispatch = defaultRegistry.dispatch; /** * Given a listener function, the function will be called any time the state value - * of one of the registered stores has changed. This function returns a `unsubscribe` - * function used to stop the subscription. + * of one of the registered stores has changed. If you specify the optional + * `storeNameOrDescriptor` parameter, the listener function will be called only + * on updates on that one specific registered store. * - * @param {Function} listener Callback function. + * This function returns an `unsubscribe` function used to stop the subscription. + * + * @param {Function} listener Callback function. + * @param {string|StoreDescriptor?} storeNameOrDescriptor Optional store name. * * @example * ```js diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 92638b02768db9..89286f689294a5 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -43,10 +43,11 @@ import { createEmitter } from './utils/emitter'; * @property {Function} registerStore registers store. */ -function isObject( object ) { - return object !== null && typeof object === 'object'; +function getStoreName( storeNameOrDescriptor ) { + return typeof storeNameOrDescriptor === 'string' + ? storeNameOrDescriptor + : storeNameOrDescriptor.name; } - /** * Creates a new store registry, given an optional object of initial store * configurations. @@ -69,14 +70,36 @@ export function createRegistry( storeConfigs = {}, parent = null ) { } /** - * Subscribe to changes to any data. + * Subscribe to changes to any data, either in all stores in registry, or + * in one specific store. * - * @param {Function} listener Listener function. + * @param {Function} listener Listener function. + * @param {string|StoreDescriptor?} storeNameOrDescriptor Optional store name. * * @return {Function} Unsubscribe function. */ - const subscribe = ( listener ) => { - return emitter.subscribe( listener ); + const subscribe = ( listener, storeNameOrDescriptor ) => { + // subscribe to all stores + if ( ! storeNameOrDescriptor ) { + return emitter.subscribe( listener ); + } + + // subscribe to one store + const storeName = getStoreName( storeNameOrDescriptor ); + const store = stores[ storeName ]; + if ( store ) { + return store.subscribe( listener ); + } + + // Trying to access a store that hasn't been registered, + // this is a pattern rarely used but seen in some places. + // We fallback to global `subscribe` here for backward-compatibility for now. + // See https://github.com/WordPress/gutenberg/pull/27466 for more info. + if ( ! parent ) { + return emitter.subscribe( listener ); + } + + return parent.subscribe( listener, storeNameOrDescriptor ); }; /** @@ -88,9 +111,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { * @return {*} The selector's returned value. */ function select( storeNameOrDescriptor ) { - const storeName = isObject( storeNameOrDescriptor ) - ? storeNameOrDescriptor.name - : storeNameOrDescriptor; + const storeName = getStoreName( storeNameOrDescriptor ); listeningStores.add( storeName ); const store = stores[ storeName ]; if ( store ) { @@ -121,9 +142,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { * @return {Object} Each key of the object matches the name of a selector. */ function resolveSelect( storeNameOrDescriptor ) { - const storeName = isObject( storeNameOrDescriptor ) - ? storeNameOrDescriptor.name - : storeNameOrDescriptor; + const storeName = getStoreName( storeNameOrDescriptor ); listeningStores.add( storeName ); const store = stores[ storeName ]; if ( store ) { @@ -145,9 +164,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { * @return {Object} Object containing the store's suspense-wrapped selectors. */ function suspendSelect( storeNameOrDescriptor ) { - const storeName = isObject( storeNameOrDescriptor ) - ? storeNameOrDescriptor.name - : storeNameOrDescriptor; + const storeName = getStoreName( storeNameOrDescriptor ); listeningStores.add( storeName ); const store = stores[ storeName ]; if ( store ) { @@ -166,9 +183,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { * @return {*} The action's returned value. */ function dispatch( storeNameOrDescriptor ) { - const storeName = isObject( storeNameOrDescriptor ) - ? storeNameOrDescriptor.name - : storeNameOrDescriptor; + const storeName = getStoreName( storeNameOrDescriptor ); const store = stores[ storeName ]; if ( store ) { return store.getActions(); @@ -268,37 +283,6 @@ export function createRegistry( storeConfigs = {}, parent = null ) { return store.store; } - /** - * Subscribe handler to a store. - * - * @param {string|StoreDescriptor} storeNameOrDescriptor The store name. - * @param {Function} handler The function subscribed to the store. - * @return {Function} A function to unsubscribe the handler. - */ - function __unstableSubscribeStore( storeNameOrDescriptor, handler ) { - const storeName = isObject( storeNameOrDescriptor ) - ? storeNameOrDescriptor.name - : storeNameOrDescriptor; - - const store = stores[ storeName ]; - if ( store ) { - return store.subscribe( handler ); - } - - // Trying to access a store that hasn't been registered, - // this is a pattern rarely used but seen in some places. - // We fallback to regular `subscribe` here for backward-compatibility for now. - // See https://github.com/WordPress/gutenberg/pull/27466 for more info. - if ( ! parent ) { - return subscribe( handler ); - } - - return parent.__unstableSubscribeStore( - storeNameOrDescriptor, - handler - ); - } - function batch( callback ) { emitter.pause(); Object.values( stores ).forEach( ( store ) => store.emitter.pause() ); @@ -321,7 +305,6 @@ export function createRegistry( storeConfigs = {}, parent = null ) { registerGenericStore, registerStore, __unstableMarkListeningStores, - __unstableSubscribeStore, }; // diff --git a/packages/data/src/test/registry.js b/packages/data/src/test/registry.js index 6a26e57c618dba..8f73b3e442900c 100644 --- a/packages/data/src/test/registry.js +++ b/packages/data/src/test/registry.js @@ -719,16 +719,15 @@ describe( 'createRegistry', () => { const listener2 = jest.fn(); // useSelect subscribes to the stores differently, // This test ensures batching works in this case as well. - const unsubscribe = registry.__unstableSubscribeStore( - 'myAwesomeReducer', - listener2 + const unsubscribe = registry.subscribe( + listener2, + 'myAwesomeReducer' ); registry.batch( () => { store.dispatch( { type: 'dummy' } ); store.dispatch( { type: 'dummy' } ); } ); unsubscribe(); - expect( listener2 ).toHaveBeenCalledTimes( 1 ); } ); } );