diff --git a/packages/data/README.md b/packages/data/README.md index e184b3328024f..d377e18addc44 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -164,43 +164,33 @@ Integrating an existing redux store with its own reducers, store enhancers and m _Example:_ ```js +import { mapValues } from 'lodash'; +import { register } from '@wordpress/data'; import existingSelectors from './existing-app/selectors'; import existingActions from './existing-app/actions'; import createStore from './existing-app/store'; -import { registerGenericStore } from 'wordpress/data'; - const reduxStore = createStore(); -const mappedSelectors = Object.keys( existingSelectors ).reduce( - ( acc, selectorKey ) => { - acc[ selectorKey ] = ( ...args ) => - existingSelectors[ selectorKey ]( reduxStore.getState(), ...args ); - return acc; - }, - {} +const boundSelectors = mapValues( + existingSelectors, + ( selector ) => ( ...args ) => selector( reduxStore.getState(), ...args ) ); -const mappedActions = Object.keys( existingActions ).reduce( - ( acc, actionKey ) => { - acc[ actionKey ] = ( ...args ) => - reduxStore.dispatch( existingActions[ actionKey ]( ...args ) ); - return acc; - }, - {} +const boundActions = mapValues( existingActions, ( action ) => ( ...args ) => + reduxStore.dispatch( action( ...args ) ) ); const genericStore = { - getSelectors() { - return mappedSelectors; - }, - getActions() { - return mappedActions; - }, - subscribe: reduxStore.subscribe, + name: 'existing-app', + instantiate: () => ( { + getSelectors: () => boundSelectors, + getActions: () => boundActions, + subscribe: reduxStore.subscribe, + } ), }; -registerGenericStore( 'existing-app', genericStore ); +register( genericStore ); ``` It is also possible to implement a completely custom store from scratch: @@ -208,39 +198,49 @@ It is also possible to implement a completely custom store from scratch: _Example:_ ```js -import { registerGenericStore } from '@wordpress/data'; - -function createCustomStore() { - let storeChanged = () => {}; - const prices = { hammer: 7.5 }; - - const selectors = { - getPrice( itemName ) { - return prices[ itemName ]; - }, - }; - - const actions = { - setPrice( itemName, price ) { - prices[ itemName ] = price; - storeChanged(); - }, - }; +import { register } from '@wordpress/data'; +function customStore() { return { - getSelectors() { - return selectors; - }, - getActions() { - return actions; - }, - subscribe( listener ) { - storeChanged = listener; + name: 'custom-data', + instantiate: () => { + const listeners = new Set(); + const prices = { hammer: 7.5 }; + + function storeChanged() { + for ( const listener of listeners ) { + listener(); + } + } + + function subscribe( listener ) { + listeners.add( listener ); + return () => listeners.delete( listener ); + } + + const selectors = { + getPrice( itemName ) { + return prices[ itemName ]; + }, + }; + + const actions = { + setPrice( itemName, price ) { + prices[ itemName ] = price; + storeChanged(); + }, + }; + + return { + getSelectors: () => selectors, + getActions: () => actions, + subscribe, + }; }, }; } -registerGenericStore( 'custom-data', createCustomStore() ); +register( customStore ); ``` ## Comparison with Redux diff --git a/packages/data/src/components/use-select/test/index.js b/packages/data/src/components/use-select/test/index.js index 4dbfe576a2f5e..b6698abf85045 100644 --- a/packages/data/src/components/use-select/test/index.js +++ b/packages/data/src/components/use-select/test/index.js @@ -723,42 +723,42 @@ describe( 'useSelect', () => { it( 'handles custom generic stores without a unsubscribe function', () => { let renderer; - function createCustomStore() { - let storeChanged = () => {}; - let counter = 0; - - const selectors = { - getCounter: () => counter, - }; - - const actions = { - increment: () => { - counter += 1; - storeChanged(); - }, - }; + const customStore = { + name: 'generic-store', + instantiate() { + let storeChanged = () => {}; + let counter = 0; + + const selectors = { + getCounter: () => counter, + }; + + const actions = { + increment: () => { + counter += 1; + storeChanged(); + }, + }; - return { - getSelectors() { - return selectors; - }, - getActions() { - return actions; - }, - subscribe( listener ) { - storeChanged = listener; - }, - }; - } + return { + getSelectors() { + return selectors; + }, + getActions() { + return actions; + }, + subscribe( listener ) { + storeChanged = listener; + }, + }; + }, + }; - registry.registerGenericStore( - 'generic-store', - createCustomStore() - ); + registry.register( customStore ); const TestComponent = jest.fn( () => { const state = useSelect( - ( select ) => select( 'generic-store' ).getCounter(), + ( select ) => select( customStore ).getCounter(), [] ); @@ -778,7 +778,7 @@ describe( 'useSelect', () => { expect( testInstance.findByType( 'div' ).props.data ).toBe( 0 ); act( () => { - registry.dispatch( 'generic-store' ).increment(); + registry.dispatch( customStore ).increment(); } ); expect( testInstance.findByType( 'div' ).props.data ).toBe( 1 ); diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 4d1626e810c87..9b9c853ca5e3d 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -3,12 +3,16 @@ */ import { mapValues, isObject, forEach } from 'lodash'; +/** + * WordPress dependencies + */ +import deprecated from '@wordpress/deprecated'; + /** * Internal dependencies */ import createReduxStore from './redux-store'; -import createCoreDataStore from './store'; -import { STORE_NAME } from './store/name'; +import coreDataStore from './store'; import { createEmitter } from './utils/emitter'; /** @typedef {import('./types').WPDataStore} WPDataStore */ @@ -159,12 +163,12 @@ export function createRegistry( storeConfigs = {}, parent = null ) { } /** - * Registers a generic store. + * Registers a store instance. * * @param {string} name Store registry name. * @param {Object} store Store instance object (getSelectors, getActions, subscribe). */ - function registerGenericStore( name, store ) { + function registerStoreInstance( name, store ) { if ( typeof store.getSelectors !== 'function' ) { throw new TypeError( 'store.getSelectors must be a function' ); } @@ -204,7 +208,35 @@ export function createRegistry( storeConfigs = {}, parent = null ) { * @param {WPDataStore} store Store definition. */ function register( store ) { - registerGenericStore( store.name, store.instantiate( registry ) ); + registerStoreInstance( store.name, store.instantiate( registry ) ); + } + + function registerGenericStore( name, store ) { + deprecated( 'wp.data.registerGenericStore', { + since: '5.9', + alternative: 'wp.data.register( storeDescriptor )', + } ); + registerStoreInstance( name, store ); + } + + /** + * Registers a standard `@wordpress/data` store. + * + * @param {string} storeName Unique namespace identifier. + * @param {Object} options Store description (reducer, actions, selectors, resolvers). + * + * @return {Object} Registered store object. + */ + function registerStore( storeName, options ) { + if ( ! options.reducer ) { + throw new TypeError( 'Must specify store reducer' ); + } + + const store = createReduxStore( storeName, options ).instantiate( + registry + ); + registerStoreInstance( storeName, store ); + return store.store; } /** @@ -240,7 +272,6 @@ export function createRegistry( storeConfigs = {}, parent = null ) { let registry = { batch, - registerGenericStore, stores, namespaces: stores, // TODO: Deprecate/remove this. subscribe, @@ -249,30 +280,12 @@ export function createRegistry( storeConfigs = {}, parent = null ) { dispatch, use, register, + registerGenericStore, + registerStore, __experimentalMarkListeningStores, __experimentalSubscribeStore, }; - /** - * Registers a standard `@wordpress/data` store. - * - * @param {string} storeName Unique namespace identifier. - * @param {Object} options Store description (reducer, actions, selectors, resolvers). - * - * @return {Object} Registered store object. - */ - registry.registerStore = ( storeName, options ) => { - if ( ! options.reducer ) { - throw new TypeError( 'Must specify store reducer' ); - } - - const store = createReduxStore( storeName, options ).instantiate( - registry - ); - registerGenericStore( storeName, store ); - return store.store; - }; - // // TODO: // This function will be deprecated as soon as it is no longer internally referenced. @@ -286,11 +299,11 @@ export function createRegistry( storeConfigs = {}, parent = null ) { return registry; } - registerGenericStore( STORE_NAME, createCoreDataStore( registry ) ); + registry.register( coreDataStore ); - Object.entries( storeConfigs ).forEach( ( [ name, config ] ) => - registry.registerStore( name, config ) - ); + for ( const [ name, config ] of Object.entries( storeConfigs ) ) { + registry.register( createReduxStore( name, config ) ); + } if ( parent ) { parent.subscribe( globalListener ); diff --git a/packages/data/src/resolvers-cache-middleware.js b/packages/data/src/resolvers-cache-middleware.js index 3f514e67ce0a1..2a1350a8bd6b9 100644 --- a/packages/data/src/resolvers-cache-middleware.js +++ b/packages/data/src/resolvers-cache-middleware.js @@ -6,7 +6,7 @@ import { get } from 'lodash'; /** * Internal dependencies */ -import { STORE_NAME } from './store/name'; +import coreDataStore from './store'; /** @typedef {import('./registry').WPDataRegistry} WPDataRegistry */ @@ -24,7 +24,7 @@ const createResolversCacheMiddleware = ( registry, reducerKey ) => () => ( next ) => ( action ) => { const resolvers = registry - .select( STORE_NAME ) + .select( coreDataStore ) .getCachedResolvers( reducerKey ); Object.entries( resolvers ).forEach( ( [ selectorName, resolversByArgs ] ) => { @@ -49,7 +49,7 @@ const createResolversCacheMiddleware = ( registry, reducerKey ) => () => ( // Trigger cache invalidation registry - .dispatch( STORE_NAME ) + .dispatch( coreDataStore ) .invalidateResolution( reducerKey, selectorName, args ); } ); } diff --git a/packages/data/src/store/index.js b/packages/data/src/store/index.js index fa153fd9b640b..27fd40ab052b3 100644 --- a/packages/data/src/store/index.js +++ b/packages/data/src/store/index.js @@ -1,53 +1,54 @@ -function createCoreDataStore( registry ) { - const getCoreDataSelector = ( selectorName ) => ( key, ...args ) => { - return registry.select( key )[ selectorName ]( ...args ); - }; +const coreDataStore = { + name: 'core/data', + instantiate( registry ) { + const getCoreDataSelector = ( selectorName ) => ( key, ...args ) => { + return registry.select( key )[ selectorName ]( ...args ); + }; - const getCoreDataAction = ( actionName ) => ( key, ...args ) => { - return registry.dispatch( key )[ actionName ]( ...args ); - }; + const getCoreDataAction = ( actionName ) => ( key, ...args ) => { + return registry.dispatch( key )[ actionName ]( ...args ); + }; - return { - getSelectors() { - return [ - 'getIsResolving', - 'hasStartedResolution', - 'hasFinishedResolution', - 'isResolving', - 'getCachedResolvers', - ].reduce( - ( memo, selectorName ) => ( { - ...memo, - [ selectorName ]: getCoreDataSelector( selectorName ), - } ), - {} - ); - }, + return { + getSelectors() { + return Object.fromEntries( + [ + 'getIsResolving', + 'hasStartedResolution', + 'hasFinishedResolution', + 'isResolving', + 'getCachedResolvers', + ].map( ( selectorName ) => [ + selectorName, + getCoreDataSelector( selectorName ), + ] ) + ); + }, - getActions() { - return [ - 'startResolution', - 'finishResolution', - 'invalidateResolution', - 'invalidateResolutionForStore', - 'invalidateResolutionForStoreSelector', - ].reduce( - ( memo, actionName ) => ( { - ...memo, - [ actionName ]: getCoreDataAction( actionName ), - } ), - {} - ); - }, + getActions() { + return Object.fromEntries( + [ + 'startResolution', + 'finishResolution', + 'invalidateResolution', + 'invalidateResolutionForStore', + 'invalidateResolutionForStoreSelector', + ].map( ( actionName ) => [ + actionName, + getCoreDataAction( actionName ), + ] ) + ); + }, - subscribe() { - // There's no reasons to trigger any listener when we subscribe to this store - // because there's no state stored in this store that need to retrigger selectors - // if a change happens, the corresponding store where the tracking stated live - // would have already triggered a "subscribe" call. - return () => {}; - }, - }; -} + subscribe() { + // There's no reasons to trigger any listener when we subscribe to this store + // because there's no state stored in this store that need to retrigger selectors + // if a change happens, the corresponding store where the tracking stated live + // would have already triggered a "subscribe" call. + return () => () => {}; + }, + }; + }, +}; -export default createCoreDataStore; +export default coreDataStore; diff --git a/packages/data/src/store/name.js b/packages/data/src/store/name.js deleted file mode 100644 index a50bd51230096..0000000000000 --- a/packages/data/src/store/name.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * The identifier for the core/data store. - * - * @type {string} - */ -export const STORE_NAME = 'core/data'; diff --git a/packages/data/src/test/registry.js b/packages/data/src/test/registry.js index 58fa9ebcb326f..24fd764f6335b 100644 --- a/packages/data/src/test/registry.js +++ b/packages/data/src/test/registry.js @@ -9,7 +9,7 @@ import { castArray, mapValues } from 'lodash'; import { createRegistry } from '../registry'; import { createRegistrySelector } from '../factory'; import createReduxStore from '../redux-store'; -import { STORE_NAME } from '../store/name'; +import coreDataStore from '../store'; jest.useFakeTimers(); @@ -72,6 +72,7 @@ describe( 'createRegistry', () => { subscribe, } ) ).toThrow(); + expect( console ).toHaveWarned(); } ); describe( 'getSelectors', () => { @@ -377,7 +378,7 @@ describe( 'createRegistry', () => { () => registry.select( 'demo' ).getValue() === 'OK', () => registry - .select( STORE_NAME ) + .select( coreDataStore ) .hasFinishedResolution( 'demo', 'getValue' ), ] ); @@ -404,7 +405,7 @@ describe( 'createRegistry', () => { () => registry.select( 'demo' ).getValue() === 'OK', () => registry - .select( STORE_NAME ) + .select( coreDataStore ) .hasFinishedResolution( 'demo', 'getValue' ), ] ); @@ -414,31 +415,6 @@ describe( 'createRegistry', () => { return promise; } ); - it( 'should resolve promise non-action to dispatch', () => { - let shouldThrow = false; - registry.registerStore( 'demo', { - reducer: ( state = 'OK' ) => { - if ( shouldThrow ) { - throw 'Should not have dispatched'; - } - - return state; - }, - selectors: { - getValue: ( state ) => state, - }, - resolvers: { - getValue: () => Promise.resolve(), - }, - } ); - shouldThrow = true; - - registry.select( 'demo' ).getValue(); - jest.runAllTimers(); - - return new Promise( ( resolve ) => process.nextTick( resolve ) ); - } ); - it( 'should not dispatch resolved promise action on subsequent selector calls', () => { registry.registerStore( 'demo', { reducer: ( state = 'NOTOK', action ) => { @@ -807,15 +783,19 @@ describe( 'createRegistry', () => { const getSelectors = () => ( { mySelector } ); const getActions = () => ( { myAction } ); const subscribe = () => {}; - registry.registerGenericStore( 'store', { - getSelectors, - getActions, - subscribe, - } ); + const myStore = { + name: 'store', + instantiate: () => ( { + getSelectors, + getActions, + subscribe, + } ), + }; + registry.register( myStore ); const subRegistry = createRegistry( {}, registry ); - subRegistry.select( 'store' ).mySelector(); - subRegistry.dispatch( 'store' ).myAction(); + subRegistry.select( myStore ).mySelector(); + subRegistry.dispatch( myStore ).myAction(); expect( mySelector ).toHaveBeenCalled(); expect( myAction ).toHaveBeenCalled(); @@ -827,10 +807,13 @@ describe( 'createRegistry', () => { const getSelectors = () => ( { mySelector } ); const getActions = () => ( { myAction } ); const subscribe = () => {}; - registry.registerGenericStore( 'store', { - getSelectors, - getActions, - subscribe, + registry.register( { + name: 'store', + instantiate: () => ( { + getSelectors, + getActions, + subscribe, + } ), } ); const subRegistry = createRegistry( {}, registry ); @@ -839,10 +822,13 @@ describe( 'createRegistry', () => { const getSelectors2 = () => ( { mySelector: mySelector2 } ); const getActions2 = () => ( { myAction: myAction2 } ); const subscribe2 = () => {}; - subRegistry.registerGenericStore( 'store', { - getSelectors: getSelectors2, - getActions: getActions2, - subscribe: subscribe2, + subRegistry.register( { + name: 'store', + instantiate: () => ( { + getSelectors: getSelectors2, + getActions: getActions2, + subscribe: subscribe2, + } ), } ); subRegistry.select( 'store' ).mySelector();