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

Add useSelectors hook for getting a store's selectors #40201

Draft
wants to merge 2 commits into
base: trunk
Choose a base branch
from

Conversation

Inwerpsel
Copy link

@Inwerpsel Inwerpsel commented Apr 9, 2022

What?

Create a new function that can be called instead of useSelect to get a store's selectors.

export default function useSelectors( storeName ) {
	const registry = useRegistry();

	return registry.select( storeName );
}

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 inside useSelect 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?

  • Add a new function that only gets the selectors.
  • Make all usages of useSelect use this function instead.
  • Remove complexity from 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 that useSelect would preserve its duplicate function. If that's the case then this change should involve very little risk. For now I indeed kept 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.

@github-actions github-actions bot added the First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository label Apr 9, 2022
@github-actions
Copy link

github-actions bot commented Apr 9, 2022

👋 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.

@ZebulanStanphill
Copy link
Member

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 useSelect (and promote the usage of useSelectors instead) for several releases before we can actually remove it.

@skorasaurus skorasaurus added the [Type] Code Quality Issues or PRs that relate to code quality label Apr 9, 2022
@Mamaduka Mamaduka added the [Type] New API New API to be used by plugin developers or package users. label Apr 10, 2022
@Mamaduka Mamaduka requested a review from jsnajdr April 10, 2022 07:02
@Inwerpsel
Copy link
Author

@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 useSelect.

@Inwerpsel Inwerpsel force-pushed the avoid-swiss-army-knife-use-select branch from 31b0c26 to d1db6b1 Compare April 11, 2022 08:17
Copy link
Member

@jsnajdr jsnajdr left a 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.

@@ -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.
Copy link
Member

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>; }`
Copy link
Member

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.
@Inwerpsel Inwerpsel force-pushed the avoid-swiss-army-knife-use-select branch from d1db6b1 to 1e4363a Compare April 11, 2022 08:31
@Inwerpsel
Copy link
Author

Thanks for the quick feedback @jsnajdr! I had just removed the changes from useSelect about the same time, but I might re-add them based on the suggestions.

is already very concise and does the same thing

I also considered this, though a custom hook is even slightly more concise. The change compared to useSelect is also less, you only need to use a different function. That would make it easier for people to switch to the new API.

A particular hook instance starts in certain mode and stays in that mode forever.
We can exploit that to write the hook this way:

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.

@jsnajdr
Copy link
Member

jsnajdr commented Apr 11, 2022

it's picked up as a violation of the rules of hooks.

That's fine, the lint rule can't understand what we're up to. We're breaking the rules very intentionally here.

@Inwerpsel
Copy link
Author

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.
@Inwerpsel Inwerpsel force-pushed the avoid-swiss-army-knife-use-select branch from e4db283 to a7140b9 Compare April 11, 2022 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository [Type] Code Quality Issues or PRs that relate to code quality [Type] New API New API to be used by plugin developers or package users.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants