-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Add useSelectors hook for getting a store's selectors #40201
base: trunk
Are you sure you want to change the base?
Add useSelectors hook for getting a store's selectors #40201
Conversation
👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @Inwerpsel! In case you missed it, we'd love to have you join us in our Slack community, where we hold regularly weekly meetings open to anyone to coordinate with each other. If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information. |
I absolutely agree that this is something that should be done, but because of backward compatibility requirements, we will almost certainly have to deprecate the 2nd use-case of |
@ZebulanStanphill Would it make sense to make it use the new version internally and keep a copy of the older one, just for exporting the API for now? I explored this here. Seems manageable as this function is supposed to not change. Only drawback is it would probably require changing every internal import of |
31b0c26
to
d1db6b1
Compare
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'm wondering whether we need to introduce the new useSelectors
hook.
useRegistry().select( store )
is already very concise and does the same thing.
We can't remove the ! hasMappingFunction
functionality right now because of backward compatibility. We'll need to mark it as deprecated and remove only much later.
One way to simplify the implementation is to make use of the fact that consumers can continue to use useSelect
in either of the two modes, but they are extremely unlikely to switch between them at runtime. In other words, nobody ever does this:
const selectResult = useSelect( props.condition ? 'core' : ( select ) => { ... } );
A particular hook instance starts in certain mode and stays in that mode forever.
We can exploit that to write the hook this way:
function useSelect( storeOrMap ) {
const mode = typeof storeOrMap === 'function' ? 'map' : 'select';
const initialMode = useRef( mode );
if ( mode !== initialMode ) {
throw new Error( `useSelect hook tried to switch from ${ initialMode } to ${ mode } mode and changing mode is not possible.` );
}
if ( mode === 'select' ) {
return useRegistry().select( storeOrMap );
}
/* Proceed to execute the complex mapping and subscribing hook */
}
This way we can avoid having to call all the dummy hooks.
packages/data/README.md
Outdated
@@ -820,12 +820,24 @@ function Paste( { children } ) { | |||
|
|||
_Parameters_ | |||
|
|||
- _mapSelect_ `Function|StoreDescriptor|string`: Function called on every state change. The returned value is exposed to the component implementing this hook. The function receives the `registry.select` method on the first argument and the `registry` on the second argument. When a store key is passed, all selectors for the store will be returned. This is only meant for usage of these selectors in event callbacks, not for data needed to create the element tree. | |||
- _mapSelect_ `Function|StoreDescriptor|string`: Function called on every state change. The returned value is exposed to the component implementing this hook. The function receives the `registry.select` method on the first argument and the `registry` on the second argument. |
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.
With the useSelectors
behavior deprecated/removed, the mapSelect
type is now merely Function
.
|
||
_Parameters_ | ||
|
||
- _storeName_ `string`: Key of the store to get controls for. **Don't use `useSelect` for calling the selectors in the render function because your component won't re-render on a data change. You need to use useSelect in that case.** `js import { useSelect } from '@wordpress/data'; function Paste( { children } ) { const { getSettings } = useSelect( 'my-shop' ); function onPaste() { // Do something with the settings. const settings = getSettings(); } return <div onPaste={ onPaste }>{ children }</div>; }` |
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.
The storeName
type is StoreDescriptor | string
.
The `useSelect` function is used to cover 2 use cases, switching between them with the first parameter's type. The original use case is to fetch data during render. The 2nd use case simply returns a store's selectors, so they can be called in event handlers. This 2nd use case does not need most of the hooks the original use is calling. However because of how hooks work, they needed to be called anyway in this path. In the new function, the only remaining hook call for getting the store's selectors is to `useRegistry`. It also allows to simplify useSelect, which doesn't need any of the checks on the type of argument anymore, and can simply return the map output. I didn't test the impact of not having to call these hooks, but I guess it's not 0. Especially if many components use this. For now `useSelect` preserves its double function, because it is exported as a public API. In a next step it's possibel to internally already use a simpler form that does 1 thing, and keep a copy of the old one only for exporting the API before it's removed.
d1db6b1
to
1e4363a
Compare
Thanks for the quick feedback @jsnajdr! I had just removed the changes from
I also considered this, though a custom hook is even slightly more concise. The change compared to
I tried this and though it should work, it's picked up as a violation of the rules of hooks. But I agree this is the "cleanest" solution, allowing to simplify the logic already. |
That's fine, the lint rule can't understand what we're up to. We're breaking the rules very intentionally here. |
I'll try this later today/tomorrow 🤞 |
This is a deliberate rules-of-hooks violation that is safe to do because the condition stays same for each invocation, mapSelect's type never changes. Unfortunately it meant disabling the rules-of-hooks check in the whole function body, but there's no getting around that. However the chance for the rule to be needed in this function is rather small.
e4db283
to
a7140b9
Compare
What?
Create a new function that can be called instead of
useSelect
to get a store's selectors.Remove complexity from
useSelect
now that the function only does 1 thing.Why?
In the previous code, 1 function is used to cover 2 use cases, switching between them with the first parameter's type. This never changes dynamically, code never depends on the function being able to do 2 things.
The original use case is to get data needed during render.
The 2nd use case simply returns a store's selectors, so they can be called in event handlers.
This use case does not need most of the hooks the original use is calling. However because of how hooks work, they needed to be called anyway in this path.
In the new function, the only remaining hook call for getting the controls is to
useRegistry
. All the other logic insideuseSelect
is completely irrelevant.It also allows to simplify
useSelect
. It won't need any of the checks on the type of argument anymore, and can simply return the map output. For now I kept this out of this branch to make it easier to provide BC.I didn't test the impact of not having to call these hooks, but I guess it's not 0. Especially if many components use this.
How?
useSelect
use this function instead.useSelect
that was only there to make the function do 2 things.Testing Instructions
I tried to locate all relevant uses of the function, so depending on test coverage it should be relatively easy to confirm I didn't miss any.
Awaiting feedback on what to do with the public API, it could be thatFor now I indeed keptuseSelect
would preserve its duplicate function. If that's the case then this change should involve very little risk.useSelect
unchanged so that this PR is unblocked. Perhaps in a follow up it's possible to add the simplified form already and keep a copy of the older one for BC.TODO
The function is exposed with the old signature as a public API. While in practice it should work to check the argument and call the other function, it's being picked up by linting as a rules of hooks violation. Which it technically is, but in practice it only depends on the type which stays stable over time. Still seems like a non optimal solution and a bad example to set.
One option is to leave the old behavior in place in
useSelect
. That would also make it safer in case a usage was missed in this PR.Alternatively keep an old copy of
useSelect
and export that as a public API. Then internal use could already use the simplified function.