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

Introduce the concept of registry selectors #13662

Merged
merged 2 commits into from
Feb 6, 2019
Merged

Conversation

youknowriad
Copy link
Contributor

@youknowriad youknowriad commented Feb 5, 2019

In some situations, you want to build selectors that target multiple stores at the same time. Until now we were relying on the global select function but the issue is that it only targets the default registry. If we have a separate provider, this might not work as expected. In this PR, I'm introducing a createRegistrySelector helper used to mark a selector a cross-stores selector and providing a registry object.

const myRegistrySelector = createRegistrySelector( registry => (state, ...args) => {
 // Do something with registry
});

Testing instructions

  • Ensure that when pasting a link to embed, the "loading" state is still shown properly.

This is a requirement for #13088

@youknowriad youknowriad self-assigned this Feb 5, 2019
@youknowriad youknowriad added the [Package] Data /packages/data label Feb 5, 2019
Copy link
Contributor

@nerrad nerrad left a comment

Choose a reason for hiding this comment

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

Looks good except for the nitpick comment I had (approved regardless).

I'm curious, what scenarios would there be where multiple registry providers would be registered in the same session? I'm guessing this is more for plugins that might register their own registry provider (rather than the default one) and might still need to select from the default registry?

@@ -19,7 +19,7 @@ import createResolversCacheMiddleware from './resolvers-cache-middleware';
*
* @param {string} key Identifying string used for namespace and redex dev tools.
* @param {Object} options Contains reducer, actions, selectors, and resolvers.
* @param {Object} registry Temporary registry reference, required for namespace updates.
* @param {Object} registry registry reference.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick:

Suggested change
* @param {Object} registry registry reference.
* @param {Object} registry Registry reference.

@youknowriad
Copy link
Contributor Author

I'm curious, what scenarios would there be where multiple registry providers would be registered in the same session?

Actually, we have a need for it inside Gutenberg itself. Right now the implementation of the reusable blocks editor is not ideal, we're forced to include their content in the root blocks reducer. Ideally the reusable block's editor is just an embedded editor inside Gutenberg which means another registry inside the Gutenberg registry.

@nerrad
Copy link
Contributor

nerrad commented Feb 5, 2019

So code calling a global selector will still need to access whatever registry it is calling the selector from right (as illustrated by the test)?

@youknowriad
Copy link
Contributor Author

yes, that's the idea.

@nerrad
Copy link
Contributor

nerrad commented Feb 5, 2019

So is the primary benefit here so that you can import the same selectors for registration with different registries and avoid cross pollution (i.e those cross store selectors in one registry will be pulling from the state specific to the store in that registry)?

@youknowriad
Copy link
Contributor Author

This more a "fix" than an "improvement". Registries are supposed to be separate data holders and selectors pure functions to be called once the state of the registry (multiple store in the registry) changes.

If we do not ensure that we're calling a selector in a store from the same registry, we're not certain that this selector is being called properly once the state of the said store changes.

@nerrad
Copy link
Contributor

nerrad commented Feb 5, 2019

Ahh gotcha, so this is more a tightening up of things to prevent possible future bugs as further improvements are made. All clear now 👍

@nerrad
Copy link
Contributor

nerrad commented Feb 5, 2019

So when this lands, will we want to update controls.js in the @wordpress/core-data package so that instead of using the select global from the default registry, it's using createRegistrySelector?

@youknowriad
Copy link
Contributor Author

So when this lands, will we want to update controls.js in the @wordpress/core-data package so that instead of using the select global from the default registry, it's using createRegistrySelector?

This is a good question and yes we need something like createRegistrySelector for controls as well. Controls should have access to the registry to avoid calling the globals.

@nerrad
Copy link
Contributor

nerrad commented Feb 5, 2019

In that vein then, we'll probably need an equivalent creator for dispatch as well.

@gziolo gziolo added this to the 5.1 (Gutenberg) milestone Feb 5, 2019
@youknowriad youknowriad merged commit f0bb097 into master Feb 6, 2019
@youknowriad youknowriad deleted the add/registry-selectors branch February 6, 2019 09:17
Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

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

I'd wanted to have had a chance to look at it, but in reflection it's about exactly what I'd hoped to have seen for an interface. Nice 👍

I still think it'll pose a challenge for something like #13177 in following dependencies, where the only other alternative I could have imagined in my mind was one where the selector made more explicit the stores/selectors from which it needed to select dependant data.

const createStateSelector = ( selector ) => function runSelector() {
function mapSelectors( selectors, store, registry ) {
const createStateSelector = ( registeredSelector ) => function runSelector() {
const selector = registeredSelector.isRegistrySelector ? registeredSelector( registry ) : registeredSelector;
Copy link
Member

@aduth aduth Feb 6, 2019

Choose a reason for hiding this comment

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

While the logic here is fairly trivial, runSelector is our hottest path in the application.

Is it possible to assign this once when mapSelectors is first called? The registry should stay constant, correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, I originally wanted to implement this feature regardless of the store implementation (outside namespace-store) but it means the computation is done on each select so I moved it here to solve the issue but it seems like I didn't :P I just thought I did. I'll follow-up

@aduth
Copy link
Member

aduth commented Feb 6, 2019

In that vein then, we'll probably need an equivalent creator for dispatch as well.

Isn't this also a blocker for #13088 , @youknowriad ?

@youknowriad
Copy link
Contributor Author

Isn't this also a blocker for #13088 , @youknowriad ?

Probably, it means we need the registry-aware controls, I'll see what I can do.

@aduth
Copy link
Member

aduth commented Feb 11, 2019

In reflecting on this, I'm a bit afraid of the implications of what we've started to introduce, particularly with respect to:

The alternatives I see are respectively limited and more extreme, where the more extreme option is less usable but more accommodating to options for statically analyzing dependencies.

The first being: Don't pass the registry, just pass select:

export const isRequestingEmbedPreview = createDerivedSelector( ( select ) => ( state, url ) => {
	return select( 'core/data' ).isResolving( REDUCER_KEY, 'getEmbedPreview', [ url ] );
} );

The "extreme" being: Force the developer to define the stores upon which they depend:

export const isRequestingEmbedPreview = createDerivedSelector(
	// (Maybe to avoid ugliness around figuring out good variable names, we
	// still pass this argument as `select`, optionally limited to only the
	// stores which are provided in the second argument)
	( coreData ) => ( state, url ) => {
		return coreData.isResolving( REDUCER_KEY, 'getEmbedPreview', [ url ] );
	},
	[ 'core/data' ]
 );

While not totally relevant, I came to this worry as a consequence of considering the idea of cross-state history discussed elsewhere ([1] [2]), where an idea in my head had started to form around history being defined as its own store, with its own state, etc. I'd thought of it in considering how we deal with state which responds to other store's state, since I'd thought maybe of the history store's state being updated when another store changes, and whether that implies two separate store changes, and two cascading withSelect subscriber updates.

@nerrad
Copy link
Contributor

nerrad commented Feb 11, 2019

  • (e.g. the newly-introduced ability to dispatch actions from a selector)

whoah, I didn't even think of that! That could admittedly be a potential problem (dispatching an action that in turn calls the selector!).

The "extreme" being: Force the developer to define the stores upon which they depend

While the described api is a bit more verbose, I like the explicit description of what stores are dependencies. At a minimum, it's probably good to only expose the select for use here (so the first example).

I wonder in relation to the undo/redo discussion if registered stores should be given a unique id on registration and that maybe could assist with cross store history. This might introduce the need for some sort of registry state that keeps track of all the registered stores and their ids? Some potential uses for the id:

  • internal tracking of which store actions can be used for replay (i.e. track no only the action but also the store id).
registryStoreActivity = {
	[ storeAId ]: [
		UPDATE: { ...args },
		SAVE: { ...args },
	],
	[ storeBId ]: [
		SWITCH: { ...args },
		TYPING: { ...args },
	],
	all: [
   		[ storeAId, 'UPDATE', {...args} ],
		[ storeBId, 'SWITCH', { ...args } ],
		[ storeAId, 'SAVE', { ...args } ],
		[ storeBId, 'TYPING', { ...args } ],
	],
};

In the above example, individual store activity is tracked so replay can just be done on the store level. There's also an "all" key that tracks actions across all stores (so replay can be done on the global level).

@youknowriad
Copy link
Contributor Author

export const isRequestingEmbedPreview = createDerivedSelector( ( select ) => ( state, url ) => {
	return select( 'core/data' ).isResolving( REDUCER_KEY, 'getEmbedPreview', [ url ] );
} );

I like this proposal personally, because it's very similar to withSelect

@aduth
Copy link
Member

aduth commented Feb 12, 2019

I like this proposal personally, because it's very similar to withSelect

In fact I was very inclined to call it withSelect 😄 The name as proposed should be considered working; I'm not in love with it.

@aduth
Copy link
Member

aduth commented Feb 12, 2019

At a minimum, it's probably good to only expose the select for use here (so the first example).

Yes, this seems most actionable / non-controversial.

I wonder in relation to the undo/redo discussion if registered stores should be given a unique id on registration and that maybe could assist with cross store history.

I'd thought similar on a registry level. I've not yet fleshed out the implementation, but I'm inclined to see if I can avoid needing something like this. Ideally the store name, within a given registry, is sufficient. The idea of having history as a store itself can help here, since it would be unique per registry.

@nerrad
Copy link
Contributor

nerrad commented Feb 12, 2019

In fact I was very inclined to call it withSelect 😄 The name as proposed should be considered working; I'm not in love with it.

I personally am okay with select as the name. Effectively, it describes what it's doing. If you want to get very specific though maybe storeSelect?

Also related to this convo, should similar treatment be given to the createRegistryControl function? So it could be something like:

export default {
	SELECT: createRegistryControl(
		( { select } ) => ( { reducerKey, selectorName, args } ) => {
			return select( reducerKey )[ selectorName ]( ...args );
		}
	)
}

So essentially, for controls the registry exposes a subset of registry functions as an object for callbacks to receive.

@aduth
Copy link
Member

aduth commented Feb 18, 2019

In reflecting on this, I'm a bit afraid of the implications of what we've started to introduce, particularly with respect to:

Revisions per feedback continued at #13889

This was referenced Apr 30, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Data /packages/data
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants