Skip to content

Commit

Permalink
Private actions and selectors: return stable references, expose to th…
Browse files Browse the repository at this point in the history
…unks (WordPress#51051)

* Return stable references to private selectors and actions

* Expose unlocked private actions and selectors to thunks

* Add tests for private registry selectors, including one failing test

* Fix comment typo in createRegistrySelector

* Private registry selectors test: register selectors before store instantiation

* Pre-bind private selectors so that registry selectors are bound to registry

---------

Co-authored-by: Robert Anderson <[email protected]>
  • Loading branch information
2 people authored and sethrubenstein committed Jul 13, 2023
1 parent a494428 commit e47775b
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 42 deletions.
2 changes: 1 addition & 1 deletion packages/data/src/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function createRegistrySelector( registrySelector ) {

/**
* Flag indicating that the selector is a registry selector that needs the correct registry
* reference to be assigned to `selecto.registry` to make it work correctly.
* reference to be assigned to `selector.registry` to make it work correctly.
* be mapped as a registry selector.
*
* @type {boolean}
Expand Down
87 changes: 56 additions & 31 deletions packages/data/src/redux-store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ function createResolversCache() {
};
}

function createBindingCache( bind ) {
const cache = new WeakMap();

return {
get( item ) {
let boundItem = cache.get( item );
if ( ! boundItem ) {
boundItem = bind( item );
cache.set( item, boundItem );
}
return boundItem;
},
};
}

/**
* Creates a data store descriptor for the provided Redux store configuration containing
* properties describing reducer, actions, selectors, controls and resolvers.
Expand Down Expand Up @@ -147,17 +162,10 @@ export default function createReduxStore( key, options ) {
const thunkArgs = {
registry,
get dispatch() {
return Object.assign(
( action ) => store.dispatch( action ),
getActions()
);
return thunkActions;
},
get select() {
return Object.assign(
( selector ) =>
selector( store.__unstableOriginalGetState() ),
getSelectors()
);
return thunkSelectors;
},
get resolveSelect() {
return getResolveSelectors();
Expand Down Expand Up @@ -185,17 +193,22 @@ export default function createReduxStore( key, options ) {
...mapValues( options.actions, bindAction ),
};

lock(
actions,
new Proxy( privateActions, {
get: ( target, prop ) => {
const privateAction = privateActions[ prop ];
return privateAction
? bindAction( privateAction )
: actions[ prop ];
},
} )
);
const boundPrivateActions = createBindingCache( bindAction );
const allActions = new Proxy( () => {}, {
get: ( target, prop ) => {
const privateAction = privateActions[ prop ];
return privateAction
? boundPrivateActions.get( privateAction )
: actions[ prop ];
},
} );

const thunkActions = new Proxy( allActions, {
apply: ( target, thisArg, [ action ] ) =>
store.dispatch( action ),
} );

lock( actions, allActions );

function bindSelector( selector ) {
if ( selector.isRegistrySelector ) {
Expand Down Expand Up @@ -234,17 +247,29 @@ export default function createReduxStore( key, options ) {
);
}

lock(
selectors,
new Proxy( privateSelectors, {
get: ( target, prop ) => {
const privateSelector = privateSelectors[ prop ];
return privateSelector
? bindSelector( privateSelector )
: selectors[ prop ];
},
} )
);
const boundPrivateSelectors = createBindingCache( bindSelector );

// Pre-bind the private selectors that have been registered by the time of
// instantiation, so that registry selectors are bound to the registry.
for ( const privateSelector of Object.values( privateSelectors ) ) {
boundPrivateSelectors.get( privateSelector );
}

const allSelectors = new Proxy( () => {}, {
get: ( target, prop ) => {
const privateSelector = privateSelectors[ prop ];
return privateSelector
? boundPrivateSelectors.get( privateSelector )
: selectors[ prop ];
},
} );

const thunkSelectors = new Proxy( allSelectors, {
apply: ( target, thisArg, [ selector ] ) =>
selector( store.__unstableOriginalGetState() ),
} );

lock( selectors, allSelectors );

const resolveSelectors = mapResolveSelectors( selectors, store );
const suspendSelectors = mapSuspendSelectors( selectors, store );
Expand Down
96 changes: 86 additions & 10 deletions packages/data/src/test/privateAPIs.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { createRegistry } from '../registry';
import createReduxStore from '../redux-store';
import { unlock } from '../private-apis';
import { createRegistrySelector } from '../factory';

describe( 'Private data APIs', () => {
let registry;
Expand Down Expand Up @@ -32,23 +33,19 @@ describe( 'Private data APIs', () => {
getState: ( state ) => state,
},
actions: { setPublicPrice },
reducer: ( state, action ) => {
if ( action?.type === 'SET_PRIVATE_PRICE' ) {
reducer: ( state = { price: 1000, secretDiscount: 800 }, action ) => {
if ( action.type === 'SET_PRIVATE_PRICE' ) {
return {
...state,
secretDiscount: action?.price,
secretDiscount: action.price,
};
} else if ( action?.type === 'SET_PUBLIC_PRICE' ) {
} else if ( action.type === 'SET_PUBLIC_PRICE' ) {
return {
...state,
price: action?.price,
price: action.price,
};
}
return {
price: 1000,
secretDiscount: 800,
...( state || {} ),
};
return state;
},
};
function createStore() {
Expand Down Expand Up @@ -147,6 +144,16 @@ describe( 'Private data APIs', () => {
expect( unlockedSelectors.getPublicPrice() ).toEqual( 1000 );
} );

it( 'should return stable references to selectors', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
const select = unlock( registry.select( groceryStore ) );
expect( select.getPublicPrice ).toBe( select.getPublicPrice );
expect( select.getSecretDiscount ).toBe( select.getSecretDiscount );
} );

it( 'should support registerStore', () => {
const groceryStore = registry.registerStore(
storeName,
Expand Down Expand Up @@ -195,6 +202,42 @@ describe( 'Private data APIs', () => {
);
expect( subPrivateSelectors.getSecretDiscount() ).toEqual( 800 );
} );

it( 'should support private registry selectors', () => {
const groceryStore = createStore();
const otherStore = createReduxStore( 'other', {
reducer: ( state = {} ) => state,
} );
unlock( otherStore ).registerPrivateSelectors( {
getPrice: createRegistrySelector(
( select ) => () => select( groceryStore ).getPublicPrice()
),
} );
registry.register( otherStore );
const privateSelectors = unlock( registry.select( otherStore ) );
expect( privateSelectors.getPrice() ).toEqual( 1000 );
} );

it( 'should support calling a private registry selector from a public selector', () => {
const groceryStore = createStore();
const getPriceWithShipping = createRegistrySelector(
( select ) => () => select( groceryStore ).getPublicPrice() + 5
);
const store = createReduxStore( 'a', {
reducer: ( state = {} ) => state,
selectors: {
getPriceWithShippingAndTax: ( state ) =>
getPriceWithShipping( state ) * 1.1,
},
} );
unlock( store ).registerPrivateSelectors( {
getPriceWithShipping,
} );
registry.register( store );
expect(
registry.select( store ).getPriceWithShippingAndTax()
).toEqual( 1105.5 );
} );
} );

describe( 'private actions', () => {
Expand Down Expand Up @@ -263,6 +306,16 @@ describe( 'Private data APIs', () => {
).toEqual( 400 );
} );

it( 'should return stable references to actions', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateActions( {
setSecretDiscount,
} );
const disp = unlock( registry.dispatch( groceryStore ) );
expect( disp.setPublicPrice ).toBe( disp.setPublicPrice );
expect( disp.setSecretDiscount ).toBe( disp.setSecretDiscount );
} );

it( 'should dispatch public actions on the unlocked store', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateActions( {
Expand Down Expand Up @@ -294,6 +347,29 @@ describe( 'Private data APIs', () => {
).toEqual( 100 );
} );

it( 'should expose unlocked private selectors and actions to thunks', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
unlock( groceryStore ).registerPrivateActions( {
setSecretDiscount,
doubleSecretDiscount() {
return ( { dispatch, select } ) => {
dispatch.setSecretDiscount(
select.getSecretDiscount() * 2
);
};
},
} );
const privateActions = unlock( registry.dispatch( groceryStore ) );
privateActions.setSecretDiscount( 100 );
privateActions.doubleSecretDiscount();
expect(
unlock( registry.select( groceryStore ) ).getSecretDiscount()
).toEqual( 200 );
} );

it( 'should support registerStore', () => {
const groceryStore = registry.registerStore(
storeName,
Expand Down

0 comments on commit e47775b

Please sign in to comment.