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

@wordpress/data: Allow for subscribing listeners to specific stores. #15483

Closed
wants to merge 1 commit 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/data/src/namespace-store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,8 @@ export default function createNamespace( key, options, registry ) {
const state = store.__unstableOriginalGetState();
const hasChanged = state !== lastState;
lastState = state;

if ( hasChanged ) {
listener();
listener( key );
}
} );
};
Expand Down
33 changes: 25 additions & 8 deletions packages/data/src/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
* External dependencies
*/
import {
without,
mapValues,
castArray,
} from 'lodash';

/**
Expand Down Expand Up @@ -41,27 +41,44 @@ import createCoreDataStore from './store';
*/
export function createRegistry( storeConfigs = {}, parent = null ) {
const stores = {};
let listeners = [];
const listeners = new Map();

/**
* Global listener called for each store's update.
*
* @param {string} storeKey
*/
function globalListener() {
listeners.forEach( ( listener ) => listener() );
function globalListener( storeKey ) {
// Need to convert Map to array here because iterating through the Map
// iterator will pick up any nested subscriptions added DURING iteration.
// So this captures the snapshot of the contents of the map before
// iterating.
const current = Array.from( listeners.entries() );
for ( const [ dependencies, listener ] of current ) {
if ( dependencies.length === 0 || dependencies.indexOf( storeKey ) > -1 ) {
listener();
}
}
}

/**
* Subscribe to changes to any data.
*
* @param {Function} listener Listener function.
* @param {Function} listener Listener function.
* @param {Array} storeDependencies If included, the listener will only
* be invoked when a store in this array
* has a state change.
*
* @return {Function} Unsubscribe function.
*/
const subscribe = ( listener ) => {
listeners.push( listener );
const subscribe = ( listener, storeDependencies ) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming question. I wonder if this should be dependentStores rather than what is here - storeDependencies might suggest it is dependencies for a store as opposed to a list of stores the subscription is dependent on?

storeDependencies = typeof storeDependencies !== 'undefined' ?
castArray( storeDependencies ) :
[];
listeners.set( storeDependencies, listener );

return () => {
listeners = without( listeners, listener );
listeners.delete( storeDependencies );
};
};

Expand Down
22 changes: 21 additions & 1 deletion packages/data/src/test/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,27 @@ describe( 'createRegistry', () => {
expect( incrementedValue ).toBe( 3 );
} );

it( 'only calls listeners registered to the specific store', () => {
const store1 = registry.registerStore( 'store1', {
reducer: ( state ) => state + 1,
} );
const store2 = registry.registerStore( 'store2', {
reducer: ( state ) => state + 1,
} );

const action = { type: 'dummy' };

const subscription1 = jest.fn();
const subscription2 = jest.fn();
registry.subscribe( subscription1, [ 'store1' ] );
registry.subscribe( subscription2, [ 'store2' ] );

store1.dispatch( action );
store2.dispatch( action );
expect( subscription1 ).toHaveBeenCalledTimes( 1 );
expect( subscription2 ).toHaveBeenCalledTimes( 1 );
} );

it( 'snapshots listeners on change, avoiding a later listener if subscribed during earlier callback', () => {
const store = registry.registerStore( 'myAwesomeReducer', {
reducer: ( state = 0 ) => state + 1,
Expand All @@ -506,7 +527,6 @@ describe( 'createRegistry', () => {
subscribeWithUnsubscribe( firstListener );

store.dispatch( { type: 'dummy' } );

expect( secondListener ).not.toHaveBeenCalled();
} );

Expand Down