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

useSelect: normalise getting selectors for callbacks #31078

Merged
merged 4 commits into from
Apr 22, 2021

Conversation

ellatrix
Copy link
Member

@ellatrix ellatrix commented Apr 22, 2021

Description

The pattern of just getting selectors to be used for callbacks (vs for the render tree) becomes more widely used, yet it almost feels discouraged, wrong or awkward to return selectors in a select map instead of calling them immediately. useSelect has mostly been used that way: call selectors and return the result. Returning the selector functions almost feels like a hack that you're not supposed to do.

This PR adjusts useSelect to allow getting selectors in a way that's less verbose and that "feels" right. It's similar to how useDispatch works.

const { getBlockCount } = useSelect( 'core/block-editor' );

Like this, you can get the selectors and call them in callbacks without it looking awkward and encourages this pattern to improve performance. We should generally avoid calling selectors more often if the result is not going to be used in the render tree.

With this pattern, you also don't have to provide dependencies since it will always be empty ([]).

Additionally, we can make some performance optimisations internally in useSelect when you're not mapping the results. We can just skip all mapping logic.

How has this been tested?

Everything should work as before.

Screenshots

Types of changes

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • I've tested my changes with keyboard and screen readers.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR (please manually search all *.native.js files for terms that need renaming or removal).

@ellatrix ellatrix added [Type] Enhancement A suggestion for improvement. [Type] Performance Related to performance efforts [Type] Code Quality Issues or PRs that relate to code quality [Package] Data /packages/data labels Apr 22, 2021
@ellatrix ellatrix requested a review from nerrad as a code owner April 22, 2021 10:03
getBlockRootClientId: selectors.getBlockRootClientId,
getBlockIndex: selectors.getBlockIndex,
getBlockCount: selectors.getBlockCount,
getDraggedBlockClientIds: selectors.getDraggedBlockClientIds,
Copy link
Member Author

Choose a reason for hiding this comment

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

Especially here, the benefits are clear.

[]
);
const { getBlockType } = useSelect(
( select ) => select( blocksStore ),
Copy link
Member Author

Choose a reason for hiding this comment

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

And here too. We're returning the entire selector list.

@ellatrix ellatrix requested a review from gziolo April 22, 2021 10:10
@github-actions
Copy link

github-actions bot commented Apr 22, 2021

Size Change: -857 B (0%)

Total Size: 1.47 MB

Filename Size Change
build/block-editor/index.js 131 kB +1.03 kB (+1%)
build/block-editor/style-rtl.css 13 kB +449 B (+4%)
build/block-editor/style.css 13 kB +445 B (+4%)
build/block-library/blocks/query/editor-rtl.css 131 B -679 B (-84%) 🏆
build/block-library/blocks/query/editor.css 132 B -677 B (-84%) 🏆
build/block-library/editor-rtl.css 9.47 kB -359 B (-4%)
build/block-library/editor.css 9.46 kB -361 B (-4%)
build/block-library/index.js 153 kB -929 B (-1%)
build/components/index.js 285 kB +21 B (0%)
build/compose/index.js 11.6 kB -1 B (0%)
build/core-data/index.js 17 kB -152 B (-1%)
build/customize-widgets/index.js 8.27 kB +21 B (0%)
build/customize-widgets/style-rtl.css 666 B +36 B (+6%) 🔍
build/customize-widgets/style.css 667 B +36 B (+6%) 🔍
build/data-controls/index.js 836 B -1 B (0%)
build/data/index.js 9.2 kB +325 B (+4%)
build/date/index.js 31.9 kB -3 B (0%)
build/dom-ready/index.js 576 B -1 B (0%)
build/dom/index.js 5.12 kB +1 B (0%)
build/edit-navigation/index.js 17.1 kB -1 B (0%)
build/edit-post/index.js 339 kB -47 B (0%)
build/edit-site/index.js 28.9 kB -15 B (0%)
build/editor/index.js 42.6 kB +1 B (0%)
build/format-library/index.js 6.77 kB +1 B (0%)
build/keyboard-shortcuts/index.js 2.53 kB +1 B (0%)
build/keycodes/index.js 1.95 kB +1 B (0%)
build/media-utils/index.js 5.39 kB -1 B (0%)
build/notices/index.js 1.85 kB +1 B (0%)
build/primitives/index.js 1.42 kB +1 B (0%)
build/react-i18n/index.js 1.45 kB -1 B (0%)
build/redux-routine/index.js 2.84 kB +2 B (0%)
build/server-side-render/index.js 2.6 kB -1 B (0%)
build/shortcode/index.js 1.7 kB +1 B (0%)
build/warning/index.js 1.14 kB -1 B (0%)
ℹ️ View Unchanged
Filename Size Change
build/a11y/index.js 1.14 kB 0 B
build/annotations/index.js 3.78 kB 0 B
build/api-fetch/index.js 3.41 kB 0 B
build/autop/index.js 2.83 kB 0 B
build/blob/index.js 664 B 0 B
build/block-directory/index.js 8.62 kB 0 B
build/block-directory/style-rtl.css 1 kB 0 B
build/block-directory/style.css 1.01 kB 0 B
build/block-library/blocks/archives/editor-rtl.css 61 B 0 B
build/block-library/blocks/archives/editor.css 60 B 0 B
build/block-library/blocks/audio/editor-rtl.css 58 B 0 B
build/block-library/blocks/audio/editor.css 58 B 0 B
build/block-library/blocks/audio/style-rtl.css 112 B 0 B
build/block-library/blocks/audio/style.css 112 B 0 B
build/block-library/blocks/block/editor-rtl.css 161 B 0 B
build/block-library/blocks/block/editor.css 161 B 0 B
build/block-library/blocks/button/editor-rtl.css 475 B 0 B
build/block-library/blocks/button/editor.css 474 B 0 B
build/block-library/blocks/button/style-rtl.css 503 B 0 B
build/block-library/blocks/button/style.css 503 B 0 B
build/block-library/blocks/buttons/editor-rtl.css 315 B 0 B
build/block-library/blocks/buttons/editor.css 315 B 0 B
build/block-library/blocks/buttons/style-rtl.css 368 B 0 B
build/block-library/blocks/buttons/style.css 368 B 0 B
build/block-library/blocks/calendar/style-rtl.css 208 B 0 B
build/block-library/blocks/calendar/style.css 208 B 0 B
build/block-library/blocks/categories/editor-rtl.css 84 B 0 B
build/block-library/blocks/categories/editor.css 83 B 0 B
build/block-library/blocks/categories/style-rtl.css 79 B 0 B
build/block-library/blocks/categories/style.css 79 B 0 B
build/block-library/blocks/code/style-rtl.css 90 B 0 B
build/block-library/blocks/code/style.css 90 B 0 B
build/block-library/blocks/columns/editor-rtl.css 190 B 0 B
build/block-library/blocks/columns/editor.css 190 B 0 B
build/block-library/blocks/columns/style-rtl.css 436 B 0 B
build/block-library/blocks/columns/style.css 435 B 0 B
build/block-library/blocks/cover/editor-rtl.css 605 B 0 B
build/block-library/blocks/cover/editor.css 605 B 0 B
build/block-library/blocks/cover/style-rtl.css 1.23 kB 0 B
build/block-library/blocks/cover/style.css 1.23 kB 0 B
build/block-library/blocks/embed/editor-rtl.css 486 B 0 B
build/block-library/blocks/embed/editor.css 486 B 0 B
build/block-library/blocks/embed/style-rtl.css 401 B 0 B
build/block-library/blocks/embed/style.css 400 B 0 B
build/block-library/blocks/file/editor-rtl.css 301 B 0 B
build/block-library/blocks/file/editor.css 300 B 0 B
build/block-library/blocks/file/frontend.js 765 B 0 B
build/block-library/blocks/file/style-rtl.css 255 B 0 B
build/block-library/blocks/file/style.css 255 B 0 B
build/block-library/blocks/freeform/editor-rtl.css 2.44 kB 0 B
build/block-library/blocks/freeform/editor.css 2.44 kB 0 B
build/block-library/blocks/gallery/editor-rtl.css 704 B 0 B
build/block-library/blocks/gallery/editor.css 705 B 0 B
build/block-library/blocks/gallery/style-rtl.css 1.09 kB 0 B
build/block-library/blocks/gallery/style.css 1.09 kB 0 B
build/block-library/blocks/group/editor-rtl.css 160 B 0 B
build/block-library/blocks/group/editor.css 160 B 0 B
build/block-library/blocks/group/style-rtl.css 57 B 0 B
build/block-library/blocks/group/style.css 57 B 0 B
build/block-library/blocks/heading/editor-rtl.css 129 B 0 B
build/block-library/blocks/heading/editor.css 129 B 0 B
build/block-library/blocks/heading/style-rtl.css 76 B 0 B
build/block-library/blocks/heading/style.css 76 B 0 B
build/block-library/blocks/html/editor-rtl.css 281 B 0 B
build/block-library/blocks/html/editor.css 281 B 0 B
build/block-library/blocks/image/editor-rtl.css 717 B 0 B
build/block-library/blocks/image/editor.css 716 B 0 B
build/block-library/blocks/image/style-rtl.css 476 B 0 B
build/block-library/blocks/image/style.css 478 B 0 B
build/block-library/blocks/latest-comments/style-rtl.css 281 B 0 B
build/block-library/blocks/latest-comments/style.css 282 B 0 B
build/block-library/blocks/latest-posts/editor-rtl.css 137 B 0 B
build/block-library/blocks/latest-posts/editor.css 137 B 0 B
build/block-library/blocks/latest-posts/style-rtl.css 523 B 0 B
build/block-library/blocks/latest-posts/style.css 522 B 0 B
build/block-library/blocks/legacy-widget/editor-rtl.css 398 B 0 B
build/block-library/blocks/legacy-widget/editor.css 399 B 0 B
build/block-library/blocks/list/style-rtl.css 63 B 0 B
build/block-library/blocks/list/style.css 63 B 0 B
build/block-library/blocks/media-text/editor-rtl.css 191 B 0 B
build/block-library/blocks/media-text/editor.css 191 B 0 B
build/block-library/blocks/media-text/style-rtl.css 535 B 0 B
build/block-library/blocks/media-text/style.css 532 B 0 B
build/block-library/blocks/more/editor-rtl.css 434 B 0 B
build/block-library/blocks/more/editor.css 434 B 0 B
build/block-library/blocks/navigation-link/editor-rtl.css 597 B 0 B
build/block-library/blocks/navigation-link/editor.css 597 B 0 B
build/block-library/blocks/navigation-link/style-rtl.css 1.07 kB 0 B
build/block-library/blocks/navigation-link/style.css 1.07 kB 0 B
build/block-library/blocks/navigation/editor-rtl.css 1.24 kB 0 B
build/block-library/blocks/navigation/editor.css 1.24 kB 0 B
build/block-library/blocks/navigation/style-rtl.css 272 B 0 B
build/block-library/blocks/navigation/style.css 271 B 0 B
build/block-library/blocks/nextpage/editor-rtl.css 395 B 0 B
build/block-library/blocks/nextpage/editor.css 395 B 0 B
build/block-library/blocks/page-list/editor-rtl.css 239 B 0 B
build/block-library/blocks/page-list/editor.css 240 B 0 B
build/block-library/blocks/page-list/style-rtl.css 167 B 0 B
build/block-library/blocks/page-list/style.css 167 B 0 B
build/block-library/blocks/paragraph/editor-rtl.css 157 B 0 B
build/block-library/blocks/paragraph/editor.css 157 B 0 B
build/block-library/blocks/paragraph/style-rtl.css 247 B 0 B
build/block-library/blocks/paragraph/style.css 248 B 0 B
build/block-library/blocks/post-author/editor-rtl.css 209 B 0 B
build/block-library/blocks/post-author/editor.css 209 B 0 B
build/block-library/blocks/post-author/style-rtl.css 183 B 0 B
build/block-library/blocks/post-author/style.css 184 B 0 B
build/block-library/blocks/post-comments-form/style-rtl.css 250 B 0 B
build/block-library/blocks/post-comments-form/style.css 250 B 0 B
build/block-library/blocks/post-content/editor-rtl.css 139 B 0 B
build/block-library/blocks/post-content/editor.css 139 B 0 B
build/block-library/blocks/post-excerpt/editor-rtl.css 73 B 0 B
build/block-library/blocks/post-excerpt/editor.css 73 B 0 B
build/block-library/blocks/post-excerpt/style-rtl.css 69 B 0 B
build/block-library/blocks/post-excerpt/style.css 69 B 0 B
build/block-library/blocks/post-featured-image/editor-rtl.css 338 B 0 B
build/block-library/blocks/post-featured-image/editor.css 338 B 0 B
build/block-library/blocks/post-featured-image/style-rtl.css 100 B 0 B
build/block-library/blocks/post-featured-image/style.css 100 B 0 B
build/block-library/blocks/post-title/style-rtl.css 60 B 0 B
build/block-library/blocks/post-title/style.css 60 B 0 B
build/block-library/blocks/preformatted/style-rtl.css 103 B 0 B
build/block-library/blocks/preformatted/style.css 103 B 0 B
build/block-library/blocks/pullquote/editor-rtl.css 183 B 0 B
build/block-library/blocks/pullquote/editor.css 183 B 0 B
build/block-library/blocks/pullquote/style-rtl.css 318 B 0 B
build/block-library/blocks/pullquote/style.css 318 B 0 B
build/block-library/blocks/query-loop/editor-rtl.css 83 B 0 B
build/block-library/blocks/query-loop/editor.css 82 B 0 B
build/block-library/blocks/query-loop/style-rtl.css 315 B 0 B
build/block-library/blocks/query-loop/style.css 317 B 0 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 122 B 0 B
build/block-library/blocks/query-pagination-numbers/editor.css 121 B 0 B
build/block-library/blocks/query-pagination/editor-rtl.css 270 B 0 B
build/block-library/blocks/query-pagination/editor.css 262 B 0 B
build/block-library/blocks/query-pagination/style-rtl.css 168 B 0 B
build/block-library/blocks/query-pagination/style.css 168 B 0 B
build/block-library/blocks/query-title/editor-rtl.css 86 B 0 B
build/block-library/blocks/query-title/editor.css 86 B 0 B
build/block-library/blocks/quote/style-rtl.css 169 B 0 B
build/block-library/blocks/quote/style.css 169 B 0 B
build/block-library/blocks/rss/editor-rtl.css 201 B 0 B
build/block-library/blocks/rss/editor.css 202 B 0 B
build/block-library/blocks/rss/style-rtl.css 290 B 0 B
build/block-library/blocks/rss/style.css 290 B 0 B
build/block-library/blocks/search/editor-rtl.css 189 B 0 B
build/block-library/blocks/search/editor.css 189 B 0 B
build/block-library/blocks/search/style-rtl.css 359 B 0 B
build/block-library/blocks/search/style.css 362 B 0 B
build/block-library/blocks/separator/editor-rtl.css 99 B 0 B
build/block-library/blocks/separator/editor.css 99 B 0 B
build/block-library/blocks/separator/style-rtl.css 251 B 0 B
build/block-library/blocks/separator/style.css 251 B 0 B
build/block-library/blocks/shortcode/editor-rtl.css 512 B 0 B
build/block-library/blocks/shortcode/editor.css 512 B 0 B
build/block-library/blocks/site-logo/editor-rtl.css 440 B 0 B
build/block-library/blocks/site-logo/editor.css 441 B 0 B
build/block-library/blocks/site-logo/style-rtl.css 154 B 0 B
build/block-library/blocks/site-logo/style.css 154 B 0 B
build/block-library/blocks/social-link/editor-rtl.css 164 B 0 B
build/block-library/blocks/social-link/editor.css 165 B 0 B
build/block-library/blocks/social-links/editor-rtl.css 796 B 0 B
build/block-library/blocks/social-links/editor.css 795 B 0 B
build/block-library/blocks/social-links/style-rtl.css 1.32 kB 0 B
build/block-library/blocks/social-links/style.css 1.33 kB 0 B
build/block-library/blocks/spacer/editor-rtl.css 308 B 0 B
build/block-library/blocks/spacer/editor.css 308 B 0 B
build/block-library/blocks/spacer/style-rtl.css 48 B 0 B
build/block-library/blocks/spacer/style.css 48 B 0 B
build/block-library/blocks/table/editor-rtl.css 478 B 0 B
build/block-library/blocks/table/editor.css 478 B 0 B
build/block-library/blocks/table/style-rtl.css 402 B 0 B
build/block-library/blocks/table/style.css 402 B 0 B
build/block-library/blocks/tag-cloud/editor-rtl.css 118 B 0 B
build/block-library/blocks/tag-cloud/editor.css 118 B 0 B
build/block-library/blocks/tag-cloud/style-rtl.css 94 B 0 B
build/block-library/blocks/tag-cloud/style.css 94 B 0 B
build/block-library/blocks/template-part/editor-rtl.css 552 B 0 B
build/block-library/blocks/template-part/editor.css 551 B 0 B
build/block-library/blocks/term-description/editor-rtl.css 90 B 0 B
build/block-library/blocks/term-description/editor.css 90 B 0 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B 0 B
build/block-library/blocks/text-columns/editor.css 95 B 0 B
build/block-library/blocks/text-columns/style-rtl.css 166 B 0 B
build/block-library/blocks/text-columns/style.css 166 B 0 B
build/block-library/blocks/verse/style-rtl.css 87 B 0 B
build/block-library/blocks/verse/style.css 87 B 0 B
build/block-library/blocks/video/editor-rtl.css 568 B 0 B
build/block-library/blocks/video/editor.css 569 B 0 B
build/block-library/blocks/video/style-rtl.css 173 B 0 B
build/block-library/blocks/video/style.css 173 B 0 B
build/block-library/common-rtl.css 1.31 kB 0 B
build/block-library/common.css 1.31 kB 0 B
build/block-library/reset-rtl.css 502 B 0 B
build/block-library/reset.css 503 B 0 B
build/block-library/style-rtl.css 9.44 kB 0 B
build/block-library/style.css 9.44 kB 0 B
build/block-library/theme-rtl.css 692 B 0 B
build/block-library/theme.css 693 B 0 B
build/block-serialization-default-parser/index.js 1.87 kB 0 B
build/block-serialization-spec-parser/index.js 3.06 kB 0 B
build/blocks/index.js 48.7 kB 0 B
build/components/style-rtl.css 16.2 kB 0 B
build/components/style.css 16.2 kB 0 B
build/deprecated/index.js 787 B 0 B
build/edit-navigation/style-rtl.css 2.86 kB 0 B
build/edit-navigation/style.css 2.86 kB 0 B
build/edit-post/classic-rtl.css 454 B 0 B
build/edit-post/classic.css 454 B 0 B
build/edit-post/style-rtl.css 6.96 kB 0 B
build/edit-post/style.css 6.95 kB 0 B
build/edit-site/style-rtl.css 4.9 kB 0 B
build/edit-site/style.css 4.89 kB 0 B
build/edit-widgets/index.js 16.7 kB 0 B
build/edit-widgets/style-rtl.css 2.97 kB 0 B
build/edit-widgets/style.css 2.98 kB 0 B
build/editor/style-rtl.css 3.9 kB 0 B
build/editor/style.css 3.9 kB 0 B
build/element/index.js 4.62 kB 0 B
build/escape-html/index.js 735 B 0 B
build/format-library/style-rtl.css 637 B 0 B
build/format-library/style.css 639 B 0 B
build/hooks/index.js 2.28 kB 0 B
build/html-entities/index.js 622 B 0 B
build/i18n/index.js 4.04 kB 0 B
build/is-shallow-equal/index.js 699 B 0 B
build/list-reusable-blocks/index.js 3.19 kB 0 B
build/list-reusable-blocks/style-rtl.css 629 B 0 B
build/list-reusable-blocks/style.css 628 B 0 B
build/nux/index.js 3.42 kB 0 B
build/nux/style-rtl.css 731 B 0 B
build/nux/style.css 727 B 0 B
build/plugins/index.js 2.95 kB 0 B
build/priority-queue/index.js 791 B 0 B
build/reusable-blocks/index.js 3.8 kB 0 B
build/reusable-blocks/style-rtl.css 225 B 0 B
build/reusable-blocks/style.css 225 B 0 B
build/rich-text/index.js 13.5 kB 0 B
build/token-list/index.js 1.27 kB 0 B
build/url/index.js 3.01 kB 0 B
build/viewport/index.js 1.85 kB 0 B
build/wordcount/index.js 1.22 kB 0 B

compressed-size-action

@gziolo
Copy link
Member

gziolo commented Apr 22, 2021

There are two sides to this proposal:

  • It resolves the issue with variables shadowing when the only thing you want is to expose the bounded selector to use when dispatching action callbacks in combination. It nicely mirrors how useDispatch works when you pass the store definition as a param.
  • What are the chances that folks would get confused with the new API and start calling those selectors outside of useSelect to render data? It isn't that they can't do it now, but it's less likely because the examples included mostly explain how to return computed data from useSelect.

@youknowriad
Copy link
Contributor

youknowriad commented Apr 22, 2021

The other thing as well is that it encourages multiple useSelect calls on the same component. This might have some very small impact on perf.

} = select( blockEditorStore );

return {
canInsertBlockType: _canInsertBlockType,
Copy link
Member

@gziolo gziolo Apr 22, 2021

Choose a reason for hiding this comment

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

To have the full picture, this could also be refactored as:

return {
    ...pick( select( blockEditorStore ), [ 'canInsertBlockType, getBlockIndex, getClientIdsOfDescendants ),
    hasUploadPermissions: getSettings().mediaUpload,
};

Copy link
Member Author

Choose a reason for hiding this comment

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

It's doesn't solve my main problem: it's awkward to return selector functions in a mapping.

Copy link
Member

Choose a reason for hiding this comment

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

Right, it's a workaround. I think for withDispatch we were providing an 3rd argument so it could use selectors without passing it down from withSelect. Here it won't work so your exploration is really worth conisdering.

@ellatrix
Copy link
Member Author

ellatrix commented Apr 22, 2021

What are the chances that folks would get confused with the new API and start calling those selectors outside of useSelect to render data? It isn't that they can't do it now, but it's less likely because the examples included mostly explain how to return computed data from useSelect.

Right, for now it's not documented and all the examples show you to map. We can add examples for this as well, but maybe use it internally only for now.

Alternatively we could create a separate experimental hook called useSelectForCallbacks or something like that to test it out, and if we want we can always merge it with useSelect later on or leave it as a separate stable hook to explicitly state the purpose of using it that way.

The other thing as well is that it encourages multiple useSelect calls on the same component. This might have some very small impact on perf.

They are not the same performance-wise. If you don't pass a mapping function, all the mapping logic is skipped so it will perform much better.

@@ -140,6 +152,10 @@ export default function useSelect( _mapSelect, deps ) {
} );

useIsomorphicLayoutEffect( () => {
if ( isWithoutMapping ) {
Copy link
Member Author

Choose a reason for hiding this comment

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

@youknowriad useSelect( storeKey) and useSelect( map ) is not the same in terms of performance because all the mapping logic is skipped. It's not the same as calling a useSelect( map ) twice.

@ellatrix
Copy link
Member Author

It seems like the question is: do we integrate this into useSelect or should we create a separate (experimental perhaps) hook called useSelectForCallbacks to make the use case clearer? Personally I don't mind either. Integrating it with useSelect just makes it more similar to useDispatch, which is nice, but the extra clarity could be welcome to avoid confusion.

@youknowriad
Copy link
Contributor

I think just integrate it with useSelect but I believe the documentation is the problem here. How can we make sure folks use it properly, is there a way to lint this somehow?

@ellatrix
Copy link
Member Author

I don't think it would be easy to write such a lint rule, but I can try. We have to check if all selector functions that are retrieved this way are called in callbacks.

@youknowriad
Copy link
Contributor

It doesn't have to be here, I'm just proposing an idea, we can start by a clear piece of documentation.

@ellatrix
Copy link
Member Author

When I look at the React lint rules for hooks, it's a bit discouraging to know that they need almost 2000 lines of code to create the exhaustive dependencies hook.

@@ -793,9 +793,25 @@ any price in the state for that currency is retrieved. If the currency prop
doesn't change and other props are passed in that do change, the price will
not change because the dependency is just the currency.

When data is only used in an event callback, the data should not be retrieved
on render, so it may be useful to get the selectors function instead.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should say something like: don't use this API while calling the selectors directly on the render function, your component won't rerender when changes happen.

Copy link
Member Author

Choose a reason for hiding this comment

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

This actually makes me wonder: why should we retrieve the selectors during render at all if we could also retrieve them in the callback itself? In other words, in the callbacks, we could just use const { selector } = select( key )?

Copy link
Contributor

Choose a reason for hiding this comment

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

The global select/dispatch/subscribe function should be avoided as much as we can. These don't rely on the RegistryProvider. For me ideally, these should become WP-only APIs added as inline styles and not something the package exports by default.

Copy link
Member Author

Choose a reason for hiding this comment

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

That would have the additional benefit of not having to pass the function as a dependency of callbacks. I'll explore this quickly in a separate PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you misread my comment, we shouldn't do it :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, Github didn't show me your comment at the time so I didn't see it :)

_Parameters_

- _\_mapSelect_ `Function`: 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.
- _\_mapSelect_ `Function|WPDataStore|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.
Copy link
Member Author

Choose a reason for hiding this comment

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

I added documentation now. I feel like we're overloading the useSelect a bit though, so this may be another argument to create a separate useSelectForCallbacks hook. It could be simpler code-wise as well since we don't need to add logic for skipping the mapping.

Copy link
Member

Choose a reason for hiding this comment

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

Either way, it would be rather confusing whatever we do at this stage. We could have useSelectors or something like that but you still need to read docs to understand the difference so maybe it's fine to add jQuery-like flexibility in terms what passed arguments trigger 😄

Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

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

Good thinking. It feels like this proposal has more benefits than drawbacks. Let's iterate on the documentation if we see reports that's is confusing.

@ellatrix
Copy link
Member Author

Thanks for the quick reviews! Let's try this and also use it anywhere we're just using the data in a callback.

@ellatrix ellatrix merged commit e07d111 into trunk Apr 22, 2021
@ellatrix ellatrix deleted the try/use-select-selectors branch April 22, 2021 15:26
@github-actions github-actions bot added this to the Gutenberg 10.6 milestone Apr 22, 2021
@nerrad
Copy link
Contributor

nerrad commented Apr 22, 2021

I think in principle this approach has merit. I'm personally leaning more towards a separate (experimental) hook to start off with. I'm wary of having significant difference of behaviour caused by different argument types passed into useSelect. Relying on documentation for the clarification seems like something that would be typically missed by folks (especially those reading through Gutenberg code to understand usage). At least with a separate hook, there's an explicit signal that it does something different than useSelect.

The other concern I have with overloading the existing function behaviour is it requires some understanding of useSelect internals to ensure its used properly (hence the need for documentation). Lint rules could help but seems like that would be overly complex for a hook with multiple behaviours.

@ellatrix
Copy link
Member Author

Right, both ways have benefits. I don't mind either way.

Copy link
Contributor

@mcsf mcsf left a comment

Choose a reason for hiding this comment

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

I think I like this. :) Spotted a mistake.

@@ -62,6 +57,7 @@ function useAutosaveNotice() {
} ),
[]
);
const { getEditedPostAttribute } = useSelect( 'core/editor' );
Copy link
Contributor

Choose a reason for hiding this comment

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

You forgot to remove getEditedPostAttribute from the previous useSelect call. :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Oops

ellatrix added a commit that referenced this pull request Apr 22, 2021
@jsnajdr
Copy link
Member

jsnajdr commented Apr 23, 2021

When calling

const { getBlockList } = useSelect( 'core/blocks' );
setTimeout( () => { console.log( getBlockList() ); }, 1000 );

the getBlockList function is somehow bound to the store state. But which state is it? The state at the time of the useSelect call, or the state at the time of the getBlockList call? I personally don't know without trying it -- both implementations are plausible.

Calling getBlockList in a React event handler callback is in principle the same: both calls also happen at different times.

If we're going to recommend this pattern, we should document how exactly the state reading is done.

@jsnajdr
Copy link
Member

jsnajdr commented Apr 23, 2021

I believe the experimental thunk actions introduced in #27276 could greatly simplify the components modified in this PR. We could verly clearly separate the data that we need to render the UI and the data that we need to perform a UI action:

const handleKeyDown = ( clientId, keyCode ) => ( { select, dispatch } ) => {
  if ( keyCode === DELETE ) {
    return dispatch.removeBlock( clientId );
  }
  if ( keyCode === DOWN ) {
    const selectedBlockClientId = select.getSelectedBlockClientId();
    dispatch.moveBlockToPosition( clientId, selectedBlockClientId );
  }
}

function BlockSelectionButton() {
  const dispatch = useDispatch( blockEditorStore );
  const onKeyDown = ( event ) => dispatch( handleKeyDown( clientId, event.keyCode ) );
  return <div onKeyDown={ onKeyDown } />;
}

This way, the handleKeyDown handler that performs a very complex data operation is completely independent from the component that uses it. Both functions do exactly the selects and dispatches they need to do their job, at exactly the right time, and no calls are wasted.

Without thunks, both functions need to be melted together in one big function, with little hope to separate them out.

@ellatrix
Copy link
Member Author

Where's the value in separating them?

And can't you do this same currently like this: handleKeyDown( clientId, event.keyCode, select, dispatch )?

@ellatrix
Copy link
Member Author

When calling

const { getBlockList } = useSelect( 'core/blocks' );
setTimeout( () => { console.log( getBlockList() ); }, 1000 );

the getBlockList function is somehow bound to the store state. But which state is it? The state at the time of the useSelect call, or the state at the time of the getBlockList call? I personally don't know without trying it -- both implementations are plausible.

Calling getBlockList in a React event handler callback is in principle the same: both calls also happen at different times.

If we're going to recommend this pattern, we should document how exactly the state reading is done.

Calling getBlockList (the selector) implies that you get the data fresh from the store, so the data at the time of calling the selector. While this is clear to me, we could mention it in the documentation too.

@jsnajdr
Copy link
Member

jsnajdr commented Apr 23, 2021

Where's the value in separating them?

It's valuable in the same way as refactoring in general is perceived as valuable. Breaking up something complex into multiple simpler parts and the composing them in a simple way. Removing code smells like "long functions" or "high cyclomatic complexity".

And can't you do this same currently like this: handleKeyDown( clientId, event.keyCode, select, dispatch )?

Where select and dispatch are the globals imported from @wordpress/data? The downside I see is that this introduces a new dependency on the global registry. Consider the following example:

import { dispatch, useSelect } from '@wordpress/data';

function onKeyDown() {
  dispatch( 'store' ).moveBlockDown();
}

function BlockList() {
  const blockList = useSelect( select => select( 'store' ).getBlockList() );
  return <ul list={ blockList } onKeyDown={ onKeyDown } />;
}

Here both functions get their data registry from two very different sources. onKeyDown accesses the global registry inside the @wordpress/data module, while BlockList uses the registry provided by React context in the React tree where it is rendered.

When dispatching a thunk action, the registry always comes from the same source.

These kinds of issues may seem minor, but can cause major headaches when trying to use the Gutenberg modules outside the Gutenberg plugin itself, in a different context and environment.

@ellatrix
Copy link
Member Author

No, I mean like this:

const handleKeyDown = ( clientId, keyCode, select, dispatch ) => {
  if ( keyCode === DELETE ) {
    return dispatch.removeBlock( clientId );
  }
  if ( keyCode === DOWN ) {
    const selectedBlockClientId = select.getSelectedBlockClientId();
    dispatch.moveBlockToPosition( clientId, selectedBlockClientId );
  }
}

function BlockSelectionButton() {
  const select = useSelect( blockEditorStore );
  const dispatch = useDispatch( blockEditorStore );
  const onKeyDown = ( event ) => handleKeyDown( clientId, event.keyCode, select, dispatch );
  return <div onKeyDown={ onKeyDown } />;
}

ellatrix added a commit that referenced this pull request Apr 23, 2021
@jsnajdr
Copy link
Member

jsnajdr commented Apr 23, 2021

No, I mean like this:

Oh yes, I see. I didn't realize that's possible. That would work 👍

youknowriad pushed a commit that referenced this pull request Apr 29, 2021
* useSelect: normalise getting selectors for callbacks

* Add docs

* Address docs feedback

* Clean up a bit
@ellatrix ellatrix mentioned this pull request May 4, 2021
7 tasks
ellatrix added a commit that referenced this pull request Aug 12, 2021
ellatrix added a commit that referenced this pull request Aug 12, 2021
@kevin940726
Copy link
Member

I just found out about this. What's the difference between useSelect( store ) and select( store )?

@youknowriad
Copy link
Contributor

@kevin940726 I'm assuming you're asking about the global exported "select" function. Both global select, dispatch and subscribe should be considered bad patterns because they only work on the default registry and not on custom RegistryProvider's and by extension custom BlockEditorProvider's.

@kevin940726
Copy link
Member

@youknowriad Thanks for the explanation! Is it documented anywhere? Have we considered deprecating global select (and dispatch, subscribe) if they are considered bad patterns?

@youknowriad
Copy link
Contributor

@kevin940726 For a lot of third-party plugins, it's the only way to extend the editor (access and manipulate data) in the editor. I'd say these functions only make sense in the context of WordPress but not in the context of the packages (core gutenberg or third-party JS usage). I guess this means we can't deprecate them officially but yeah mentioning this at least in the architecture docs would be great.

I think we can also consider moving the declaration of these function to add_inline_script potentially (php) to clarify that these are WP specific.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Data /packages/data [Type] Code Quality Issues or PRs that relate to code quality [Type] Enhancement A suggestion for improvement. [Type] Performance Related to performance efforts
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants