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

Private actions and selectors: return stable references, expose to thunks #51051

Merged
merged 6 commits into from
Jun 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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