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

@wordpress/data: Create useSelect hook. #15512

Closed
wants to merge 4 commits into from
Closed

Conversation

nerrad
Copy link
Contributor

@nerrad nerrad commented May 8, 2019

Description

See #15473 for background. The purpose of this pull is to do some experiments on a new useSelect hook. In this initial iteration:

  • Signature is useSelect( storeName: string ): Object
  • The hook receives a store name and will return an enhanced object of selectors from the store which themselves are hooks.

Example usage:

/**
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';
export default function PostTrashCheck( { children } ) {
const { isEditedPostNew, getCurrentPostId } = useSelect( 'core/editor' );
const isNew = isEditedPostNew();
const postId = getCurrentPostId();
if ( isNew || ! postId ) {
return null;
}
return children;
}

Rationale:

  • The api is fairly intuitive and easy to consume.
  • In following the hook pattern, each selector on a store is enhanced to be itself a hook so that they are individually subscribed to changes in selector state. This eliminates the need to provide a mapSelectToProps callback (although I do think we'll want something like that... see notes later).
  • Edit, this point and the example below actually break one of the rules of Hooks so it's not a valid point/example. Subscriptions are not set until a returned enhanced selector is invoked. This does allow for some lazy subscription registration on selector calls. Example:
const myComponent = ( propA ) => {
    const { mySelectorA, mySelectorB } = useSelect( 'mystore' );
    // a subscription is created for the mySelectorB call.
    const valueB = mySelectorB();
    let valueA = null;
    if ( propA === 'foo' ) {
          // subscription doesn't get set until this condition is met
          valueA = mySelectorA();
    }
    /** ... rest of logic for component **/
}

Some things to consider & potential drawbacks:

  • Although subscriptions are not set until the enhanced selector is invoked, there is a bit more overhead involved than a single subscription created (as in withSelect). For instance, one of the concerns here is that with multiple selectHooks with resolvers, there's more re-rendering that could happen as each selector is resolved.
  • I'm not certain if the async context is needed here but obviously this would need reworked if we want to retain that.
  • Related to the previous point and the proposed signatures listed in Block Template Warning Missing in 5.2 #15743, we may want to offer the alternative mapSelectToProps option as that would reduce the number of registry subscriptions. However in the case of providing a mapSelectToProps, the returned object would be the props returned from the mapSelectToProps function. This version would be more similar to the current behaviour of withSelect
  • It's convention that hooks have the use prefix. Although I didn't do that there, there's some advantages to returning our enhanced selectors with that prefix because it: 1. makes it clear these are hooks and should be treated as such. 2. Exposes them to the eslinting on hooks (which helps prevents incorrect usage - such as implementation order matters).

Todos:

  • evaluate current approach, get feedback.
  • implement additional signatures outlined in Data: Implement useSelect hooks corollary to withSelect #15473. I'm thinking it should be pretty consistent that if you provide a function it is assumed it is a mapSelectToProps like callback and if its a string, it's assumed to be a store name (and thus return the enhanced selectors on that store).
  • Improve withSelect to incorporate a similar signature as what we land on for useSelect (although I don't think we need to strive for complete consistency because there is varying paradigms in play here).
  • Add tests
  • Pick a component which should help us gauge performance impact to refactor as a part of this work?
  • Benchmarks (what existing benchmarking tools do we have for the project that can be used?).
  • Changelog/docs.

Dependencies for this work:

How has this been tested?

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.

nerrad added 4 commits May 8, 2019 14:27
Thiis receives a store name and returns enhanced selectors for the given store:

- an object is returned of all new selector hooks.
- Each selector has been converted to a custom hook that has its own subscription to the store state.
To demonstrate its viability.
@nerrad nerrad added [Package] Data Controls /packages/data-controls [Type] Enhancement A suggestion for improvement. labels May 8, 2019
@nerrad nerrad self-assigned this May 8, 2019
@aduth
Copy link
Member

aduth commented May 8, 2019

const unsubscribe = subscribe( () => {
setResult( selector( ...args ) );
} );
return () => unsubscribe();
Copy link
Contributor Author

@nerrad nerrad May 9, 2019

Choose a reason for hiding this comment

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

one thing I'd like to explore here once #15436 is available, is if the selector has a resolver and the return is undefined then don't set the state. This would prevent unnecessary renders while the selector is resolving. It effectively would normalize undefined returns to null for all selectors with resolvers. That's one issue I've seen in the wild with withSelect currently in that unless the mapping function handles undefined results, re-renders are more frequent.


function PostTrashCheck( { isNew, postId, children } ) {
export default function PostTrashCheck( { children } ) {
const { isEditedPostNew, getCurrentPostId } = useSelect( 'core/editor' );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Convention for hooks is kinda broken here, so I think I'll have to make sure that we return the custom enhanced selector hooks with the use prefix. So this would become something like:

const { useIsEditedPostNew, useGetCurrentPostId } = useSelect( 'core/editor' );

Necessary because otherwise it won't be obvious (without knowing the internals ) that these are in fact react hooks and thus the rules of Hooks apply to them.

@nerrad
Copy link
Contributor Author

nerrad commented May 22, 2019

closing this in favour of the approach happening on #15737

@nerrad nerrad closed this May 22, 2019
@nerrad nerrad deleted the FET/create-useSelect branch May 22, 2019 00:03
@nerrad nerrad mentioned this pull request Nov 6, 2020
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Data Controls /packages/data-controls [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants