-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Conversation
There was a problem hiding this 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?
packages/data/src/namespace-store.js
Outdated
@@ -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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick:
* @param {Object} registry registry reference. | |
* @param {Object} registry Registry reference. |
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. |
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)? |
yes, that's the idea. |
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)? |
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. |
Ahh gotcha, so this is more a tightening up of things to prevent possible future bugs as further improvements are made. All clear now 👍 |
So when this lands, will we want to update |
This is a good question and yes we need something like |
In that vein then, we'll probably need an equivalent creator for |
There was a problem hiding this 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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
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. |
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 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 |
whoah, I didn't even think of that! That could admittedly be a potential problem (dispatching an action that in turn calls the selector!).
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 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:
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). |
I like this proposal personally, because it's very similar to |
In fact I was very inclined to call it |
Yes, this seems most actionable / non-controversial.
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. |
I personally am okay with Also related to this convo, should similar treatment be given to the 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. |
Revisions per feedback continued at #13889 |
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 acreateRegistrySelector
helper used to mark a selector a cross-stores selector and providing a registry object.Testing instructions
This is a requirement for #13088