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 suspense support to the data module #37261

Merged
merged 3 commits into from
May 9, 2022
Merged

Add suspense support to the data module #37261

merged 3 commits into from
May 9, 2022

Conversation

youknowriad
Copy link
Contributor

Be prepared for some ugly code, bug ugly code for pocs is always the best kind of code.

Anyway, the idea here is simple, instead of doing useSelect, we should be able to do useSuspenseSelect in order to have the exact same result but for any thing that is async (in our case, it means for anything that has a resolver), suspend the rendering and tell react that it's not ready yet.

In other words, add Suspense support to the data module.

What this allows us to do?

Right now, when loading the editor (or more visibility in the site editor), there's a waterfall effect, the template is loaded and then rendered with a bunch of spinners and then the template parts get loaded and then site logo shows a spinner, same for query loops... until everything is loaded.

With suspense we can declaratively say, show this fallback loading state until everything is ready pretty easily. You can try the site editor in this PR to see how it compared to trunk.

There are still some glitches right now that I haven't been able to pinpoint yet. Basically when the template parts loads, there's a very small delay of some milliseconds that pass before the site logo block get rendered suspending the rendering once more, this ends up with a glitch showing the loading state twice separated with a very small delay where the actual components in their temporary state are rendered. I think this should be solvable but we need to figure out where the asyncness is happening to be able to do that.

Open questions

  • This implements suspense support in the data module using the experimental APIs that are present in React 17 (throwing promises) but I'm not clear yet whether React 18 will make changes there so I'm not sure yet if we should proceed there before the React 18 upgrade.

@youknowriad youknowriad added [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible Framework Issues related to broader framework topics, especially as it relates to javascript [Package] Data /packages/data labels Dec 9, 2021
*
* @return {Function} A custom react hook.
*/
export function useSuspenseSelect( mapSelect, deps ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is basically the exact same code as useSelect, the only differences are the places where we throw shouldBeSuspended

Copy link
Contributor

Choose a reason for hiding this comment

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

I know it's a draft so at the risk of stating the obvious: It would be nice to reduce the duplication somehow. Maybe an option like isSuspense, or maybe a common abstraction?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What would be cool would be to automatically fallback to non-suspense version if the Suspense provider is detected (or opposite thought not sure about auto-suspending everything :P )

@github-actions
Copy link

github-actions bot commented Dec 9, 2021

Size Change: +325 B (0%)

Total Size: 1.24 MB

Filename Size Change
build/data/index.min.js 7.98 kB +325 B (+4%)
ℹ️ View Unchanged
Filename Size
build/a11y/index.min.js 993 B
build/annotations/index.min.js 2.77 kB
build/api-fetch/index.min.js 2.27 kB
build/autop/index.min.js 2.15 kB
build/blob/index.min.js 487 B
build/block-directory/index.min.js 6.51 kB
build/block-directory/style-rtl.css 1.01 kB
build/block-directory/style.css 1.01 kB
build/block-editor/default-editor-styles-rtl.css 378 B
build/block-editor/default-editor-styles.css 378 B
build/block-editor/index.min.js 150 kB
build/block-editor/style-rtl.css 15 kB
build/block-editor/style.css 15 kB
build/block-library/blocks/archives/editor-rtl.css 61 B
build/block-library/blocks/archives/editor.css 60 B
build/block-library/blocks/archives/style-rtl.css 65 B
build/block-library/blocks/archives/style.css 65 B
build/block-library/blocks/audio/editor-rtl.css 150 B
build/block-library/blocks/audio/editor.css 150 B
build/block-library/blocks/audio/style-rtl.css 111 B
build/block-library/blocks/audio/style.css 111 B
build/block-library/blocks/audio/theme-rtl.css 125 B
build/block-library/blocks/audio/theme.css 125 B
build/block-library/blocks/avatar/editor-rtl.css 116 B
build/block-library/blocks/avatar/editor.css 116 B
build/block-library/blocks/avatar/style-rtl.css 59 B
build/block-library/blocks/avatar/style.css 59 B
build/block-library/blocks/block/editor-rtl.css 161 B
build/block-library/blocks/block/editor.css 161 B
build/block-library/blocks/button/editor-rtl.css 445 B
build/block-library/blocks/button/editor.css 445 B
build/block-library/blocks/button/style-rtl.css 560 B
build/block-library/blocks/button/style.css 560 B
build/block-library/blocks/buttons/editor-rtl.css 292 B
build/block-library/blocks/buttons/editor.css 292 B
build/block-library/blocks/buttons/style-rtl.css 275 B
build/block-library/blocks/buttons/style.css 275 B
build/block-library/blocks/calendar/style-rtl.css 207 B
build/block-library/blocks/calendar/style.css 207 B
build/block-library/blocks/categories/editor-rtl.css 84 B
build/block-library/blocks/categories/editor.css 83 B
build/block-library/blocks/categories/style-rtl.css 79 B
build/block-library/blocks/categories/style.css 79 B
build/block-library/blocks/code/style-rtl.css 103 B
build/block-library/blocks/code/style.css 103 B
build/block-library/blocks/code/theme-rtl.css 124 B
build/block-library/blocks/code/theme.css 124 B
build/block-library/blocks/columns/editor-rtl.css 108 B
build/block-library/blocks/columns/editor.css 108 B
build/block-library/blocks/columns/style-rtl.css 406 B
build/block-library/blocks/columns/style.css 406 B
build/block-library/blocks/comment-author-avatar/editor-rtl.css 125 B
build/block-library/blocks/comment-author-avatar/editor.css 125 B
build/block-library/blocks/comment-content/style-rtl.css 92 B
build/block-library/blocks/comment-content/style.css 92 B
build/block-library/blocks/comment-template/style-rtl.css 127 B
build/block-library/blocks/comment-template/style.css 127 B
build/block-library/blocks/comments-pagination-numbers/editor-rtl.css 123 B
build/block-library/blocks/comments-pagination-numbers/editor.css 121 B
build/block-library/blocks/comments-pagination/editor-rtl.css 222 B
build/block-library/blocks/comments-pagination/editor.css 209 B
build/block-library/blocks/comments-pagination/style-rtl.css 235 B
build/block-library/blocks/comments-pagination/style.css 231 B
build/block-library/blocks/comments-title/editor-rtl.css 75 B
build/block-library/blocks/comments-title/editor.css 75 B
build/block-library/blocks/comments/editor-rtl.css 95 B
build/block-library/blocks/comments/editor.css 95 B
build/block-library/blocks/cover/editor-rtl.css 546 B
build/block-library/blocks/cover/editor.css 547 B
build/block-library/blocks/cover/style-rtl.css 1.53 kB
build/block-library/blocks/cover/style.css 1.53 kB
build/block-library/blocks/embed/editor-rtl.css 293 B
build/block-library/blocks/embed/editor.css 293 B
build/block-library/blocks/embed/style-rtl.css 417 B
build/block-library/blocks/embed/style.css 417 B
build/block-library/blocks/embed/theme-rtl.css 124 B
build/block-library/blocks/embed/theme.css 124 B
build/block-library/blocks/file/editor-rtl.css 300 B
build/block-library/blocks/file/editor.css 300 B
build/block-library/blocks/file/style-rtl.css 255 B
build/block-library/blocks/file/style.css 255 B
build/block-library/blocks/file/view.min.js 353 B
build/block-library/blocks/freeform/editor-rtl.css 2.44 kB
build/block-library/blocks/freeform/editor.css 2.44 kB
build/block-library/blocks/gallery/editor-rtl.css 961 B
build/block-library/blocks/gallery/editor.css 964 B
build/block-library/blocks/gallery/style-rtl.css 1.51 kB
build/block-library/blocks/gallery/style.css 1.51 kB
build/block-library/blocks/gallery/theme-rtl.css 122 B
build/block-library/blocks/gallery/theme.css 122 B
build/block-library/blocks/group/editor-rtl.css 333 B
build/block-library/blocks/group/editor.css 333 B
build/block-library/blocks/group/style-rtl.css 57 B
build/block-library/blocks/group/style.css 57 B
build/block-library/blocks/group/theme-rtl.css 78 B
build/block-library/blocks/group/theme.css 78 B
build/block-library/blocks/heading/style-rtl.css 76 B
build/block-library/blocks/heading/style.css 76 B
build/block-library/blocks/html/editor-rtl.css 332 B
build/block-library/blocks/html/editor.css 333 B
build/block-library/blocks/image/editor-rtl.css 731 B
build/block-library/blocks/image/editor.css 730 B
build/block-library/blocks/image/style-rtl.css 529 B
build/block-library/blocks/image/style.css 535 B
build/block-library/blocks/image/theme-rtl.css 124 B
build/block-library/blocks/image/theme.css 124 B
build/block-library/blocks/latest-comments/style-rtl.css 284 B
build/block-library/blocks/latest-comments/style.css 284 B
build/block-library/blocks/latest-posts/editor-rtl.css 199 B
build/block-library/blocks/latest-posts/editor.css 198 B
build/block-library/blocks/latest-posts/style-rtl.css 463 B
build/block-library/blocks/latest-posts/style.css 462 B
build/block-library/blocks/list/style-rtl.css 88 B
build/block-library/blocks/list/style.css 88 B
build/block-library/blocks/media-text/editor-rtl.css 266 B
build/block-library/blocks/media-text/editor.css 263 B
build/block-library/blocks/media-text/style-rtl.css 493 B
build/block-library/blocks/media-text/style.css 490 B
build/block-library/blocks/more/editor-rtl.css 431 B
build/block-library/blocks/more/editor.css 431 B
build/block-library/blocks/navigation-link/editor-rtl.css 708 B
build/block-library/blocks/navigation-link/editor.css 706 B
build/block-library/blocks/navigation-link/style-rtl.css 115 B
build/block-library/blocks/navigation-link/style.css 115 B
build/block-library/blocks/navigation-submenu/editor-rtl.css 299 B
build/block-library/blocks/navigation-submenu/editor.css 299 B
build/block-library/blocks/navigation-submenu/view.min.js 375 B
build/block-library/blocks/navigation/editor-rtl.css 2.03 kB
build/block-library/blocks/navigation/editor.css 2.04 kB
build/block-library/blocks/navigation/style-rtl.css 1.95 kB
build/block-library/blocks/navigation/style.css 1.94 kB
build/block-library/blocks/navigation/view-modal.min.js 2.78 kB
build/block-library/blocks/navigation/view.min.js 395 B
build/block-library/blocks/nextpage/editor-rtl.css 395 B
build/block-library/blocks/nextpage/editor.css 395 B
build/block-library/blocks/page-list/editor-rtl.css 363 B
build/block-library/blocks/page-list/editor.css 363 B
build/block-library/blocks/page-list/style-rtl.css 175 B
build/block-library/blocks/page-list/style.css 175 B
build/block-library/blocks/paragraph/editor-rtl.css 157 B
build/block-library/blocks/paragraph/editor.css 157 B
build/block-library/blocks/paragraph/style-rtl.css 260 B
build/block-library/blocks/paragraph/style.css 260 B
build/block-library/blocks/post-author/style-rtl.css 175 B
build/block-library/blocks/post-author/style.css 176 B
build/block-library/blocks/post-comments-form/editor-rtl.css 69 B
build/block-library/blocks/post-comments-form/editor.css 69 B
build/block-library/blocks/post-comments-form/style-rtl.css 495 B
build/block-library/blocks/post-comments-form/style.css 495 B
build/block-library/blocks/post-comments/editor-rtl.css 77 B
build/block-library/blocks/post-comments/editor.css 77 B
build/block-library/blocks/post-comments/style-rtl.css 583 B
build/block-library/blocks/post-comments/style.css 583 B
build/block-library/blocks/post-excerpt/editor-rtl.css 73 B
build/block-library/blocks/post-excerpt/editor.css 73 B
build/block-library/blocks/post-excerpt/style-rtl.css 69 B
build/block-library/blocks/post-excerpt/style.css 69 B
build/block-library/blocks/post-featured-image/editor-rtl.css 721 B
build/block-library/blocks/post-featured-image/editor.css 721 B
build/block-library/blocks/post-featured-image/style-rtl.css 153 B
build/block-library/blocks/post-featured-image/style.css 153 B
build/block-library/blocks/post-template/editor-rtl.css 99 B
build/block-library/blocks/post-template/editor.css 98 B
build/block-library/blocks/post-template/style-rtl.css 323 B
build/block-library/blocks/post-template/style.css 323 B
build/block-library/blocks/post-terms/style-rtl.css 73 B
build/block-library/blocks/post-terms/style.css 73 B
build/block-library/blocks/post-title/style-rtl.css 80 B
build/block-library/blocks/post-title/style.css 80 B
build/block-library/blocks/preformatted/style-rtl.css 103 B
build/block-library/blocks/preformatted/style.css 103 B
build/block-library/blocks/pullquote/editor-rtl.css 198 B
build/block-library/blocks/pullquote/editor.css 198 B
build/block-library/blocks/pullquote/style-rtl.css 370 B
build/block-library/blocks/pullquote/style.css 370 B
build/block-library/blocks/pullquote/theme-rtl.css 167 B
build/block-library/blocks/pullquote/theme.css 167 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 122 B
build/block-library/blocks/query-pagination-numbers/editor.css 121 B
build/block-library/blocks/query-pagination/editor-rtl.css 221 B
build/block-library/blocks/query-pagination/editor.css 211 B
build/block-library/blocks/query-pagination/style-rtl.css 234 B
build/block-library/blocks/query-pagination/style.css 231 B
build/block-library/blocks/query/editor-rtl.css 369 B
build/block-library/blocks/query/editor.css 369 B
build/block-library/blocks/quote/style-rtl.css 213 B
build/block-library/blocks/quote/style.css 213 B
build/block-library/blocks/quote/theme-rtl.css 223 B
build/block-library/blocks/quote/theme.css 226 B
build/block-library/blocks/read-more/style-rtl.css 132 B
build/block-library/blocks/read-more/style.css 132 B
build/block-library/blocks/rss/editor-rtl.css 202 B
build/block-library/blocks/rss/editor.css 204 B
build/block-library/blocks/rss/style-rtl.css 289 B
build/block-library/blocks/rss/style.css 288 B
build/block-library/blocks/search/editor-rtl.css 165 B
build/block-library/blocks/search/editor.css 165 B
build/block-library/blocks/search/style-rtl.css 397 B
build/block-library/blocks/search/style.css 398 B
build/block-library/blocks/search/theme-rtl.css 64 B
build/block-library/blocks/search/theme.css 64 B
build/block-library/blocks/separator/editor-rtl.css 140 B
build/block-library/blocks/separator/editor.css 140 B
build/block-library/blocks/separator/style-rtl.css 233 B
build/block-library/blocks/separator/style.css 233 B
build/block-library/blocks/separator/theme-rtl.css 194 B
build/block-library/blocks/separator/theme.css 194 B
build/block-library/blocks/shortcode/editor-rtl.css 474 B
build/block-library/blocks/shortcode/editor.css 474 B
build/block-library/blocks/site-logo/editor-rtl.css 759 B
build/block-library/blocks/site-logo/editor.css 759 B
build/block-library/blocks/site-logo/style-rtl.css 181 B
build/block-library/blocks/site-logo/style.css 181 B
build/block-library/blocks/site-tagline/editor-rtl.css 86 B
build/block-library/blocks/site-tagline/editor.css 86 B
build/block-library/blocks/site-title/editor-rtl.css 84 B
build/block-library/blocks/site-title/editor.css 84 B
build/block-library/blocks/social-link/editor-rtl.css 177 B
build/block-library/blocks/social-link/editor.css 177 B
build/block-library/blocks/social-links/editor-rtl.css 674 B
build/block-library/blocks/social-links/editor.css 673 B
build/block-library/blocks/social-links/style-rtl.css 1.37 kB
build/block-library/blocks/social-links/style.css 1.36 kB
build/block-library/blocks/spacer/editor-rtl.css 332 B
build/block-library/blocks/spacer/editor.css 332 B
build/block-library/blocks/spacer/style-rtl.css 48 B
build/block-library/blocks/spacer/style.css 48 B
build/block-library/blocks/table/editor-rtl.css 504 B
build/block-library/blocks/table/editor.css 504 B
build/block-library/blocks/table/style-rtl.css 625 B
build/block-library/blocks/table/style.css 625 B
build/block-library/blocks/table/theme-rtl.css 188 B
build/block-library/blocks/table/theme.css 188 B
build/block-library/blocks/tag-cloud/style-rtl.css 226 B
build/block-library/blocks/tag-cloud/style.css 227 B
build/block-library/blocks/template-part/editor-rtl.css 149 B
build/block-library/blocks/template-part/editor.css 149 B
build/block-library/blocks/template-part/theme-rtl.css 101 B
build/block-library/blocks/template-part/theme.css 101 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B
build/block-library/blocks/text-columns/editor.css 95 B
build/block-library/blocks/text-columns/style-rtl.css 166 B
build/block-library/blocks/text-columns/style.css 166 B
build/block-library/blocks/verse/style-rtl.css 87 B
build/block-library/blocks/verse/style.css 87 B
build/block-library/blocks/video/editor-rtl.css 571 B
build/block-library/blocks/video/editor.css 572 B
build/block-library/blocks/video/style-rtl.css 173 B
build/block-library/blocks/video/style.css 173 B
build/block-library/blocks/video/theme-rtl.css 124 B
build/block-library/blocks/video/theme.css 124 B
build/block-library/common-rtl.css 993 B
build/block-library/common.css 990 B
build/block-library/editor-rtl.css 10.2 kB
build/block-library/editor.css 10.3 kB
build/block-library/index.min.js 179 kB
build/block-library/reset-rtl.css 478 B
build/block-library/reset.css 478 B
build/block-library/style-rtl.css 11.5 kB
build/block-library/style.css 11.5 kB
build/block-library/theme-rtl.css 689 B
build/block-library/theme.css 694 B
build/block-serialization-default-parser/index.min.js 1.12 kB
build/block-serialization-spec-parser/index.min.js 2.83 kB
build/blocks/index.min.js 47 kB
build/components/index.min.js 227 kB
build/components/style-rtl.css 15 kB
build/components/style.css 15 kB
build/compose/index.min.js 11.4 kB
build/core-data/index.min.js 14.6 kB
build/customize-widgets/index.min.js 11 kB
build/customize-widgets/style-rtl.css 1.39 kB
build/customize-widgets/style.css 1.39 kB
build/data-controls/index.min.js 663 B
build/date/index.min.js 32 kB
build/deprecated/index.min.js 518 B
build/dom-ready/index.min.js 336 B
build/dom/index.min.js 4.59 kB
build/edit-navigation/index.min.js 15.8 kB
build/edit-navigation/style-rtl.css 4.05 kB
build/edit-navigation/style.css 4.05 kB
build/edit-post/classic-rtl.css 546 B
build/edit-post/classic.css 547 B
build/edit-post/index.min.js 30.1 kB
build/edit-post/style-rtl.css 7.02 kB
build/edit-post/style.css 7.02 kB
build/edit-site/index.min.js 47.4 kB
build/edit-site/style-rtl.css 7.95 kB
build/edit-site/style.css 7.93 kB
build/edit-widgets/index.min.js 16.3 kB
build/edit-widgets/style-rtl.css 4.41 kB
build/edit-widgets/style.css 4.4 kB
build/editor/index.min.js 38.4 kB
build/editor/style-rtl.css 3.67 kB
build/editor/style.css 3.67 kB
build/element/index.min.js 4.3 kB
build/escape-html/index.min.js 548 B
build/format-library/index.min.js 6.62 kB
build/format-library/style-rtl.css 571 B
build/format-library/style.css 571 B
build/hooks/index.min.js 1.66 kB
build/html-entities/index.min.js 454 B
build/i18n/index.min.js 3.79 kB
build/is-shallow-equal/index.min.js 535 B
build/keyboard-shortcuts/index.min.js 1.83 kB
build/keycodes/index.min.js 1.41 kB
build/list-reusable-blocks/index.min.js 1.75 kB
build/list-reusable-blocks/style-rtl.css 838 B
build/list-reusable-blocks/style.css 838 B
build/media-utils/index.min.js 2.94 kB
build/notices/index.min.js 957 B
build/nux/index.min.js 2.1 kB
build/nux/style-rtl.css 751 B
build/nux/style.css 749 B
build/plugins/index.min.js 1.98 kB
build/preferences-persistence/index.min.js 2.16 kB
build/preferences/index.min.js 1.32 kB
build/primitives/index.min.js 949 B
build/priority-queue/index.min.js 628 B
build/react-i18n/index.min.js 704 B
build/react-refresh-entry/index.min.js 8.44 kB
build/react-refresh-runtime/index.min.js 7.31 kB
build/redux-routine/index.min.js 2.69 kB
build/reusable-blocks/index.min.js 2.24 kB
build/reusable-blocks/style-rtl.css 256 B
build/reusable-blocks/style.css 256 B
build/rich-text/index.min.js 11.2 kB
build/server-side-render/index.min.js 1.61 kB
build/shortcode/index.min.js 1.52 kB
build/token-list/index.min.js 668 B
build/url/index.min.js 1.99 kB
build/vendors/react-dom.min.js 38.5 kB
build/vendors/react.min.js 4.34 kB
build/viewport/index.min.js 1.08 kB
build/warning/index.min.js 280 B
build/widgets/index.min.js 7.21 kB
build/widgets/style-rtl.css 1.16 kB
build/widgets/style.css 1.16 kB
build/wordcount/index.min.js 1.07 kB

compressed-size-action

@youknowriad youknowriad requested a review from adamziel December 9, 2021 18:30
@youknowriad youknowriad self-assigned this Dec 9, 2021
fallback={
<h1>
Very Ugly
Loading State
Copy link
Contributor

Choose a reason for hiding this comment

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

:D

@gziolo
Copy link
Member

gziolo commented Dec 13, 2021

I watched the talk from React Conf 2021 and it looks like there is a new API dedicated to external store libraries that resolves the tearing issue that can happen in concurrent rendering:

Screenshot 2021-12-13 at 14 56 33

Screenshot 2021-12-13 at 14 54 04

All the details can be found in the talk from Daishi Kato:

https://www.youtube.com/watch?v=oPfSC5bQPR8

I guess we already have the controlled tearing issue when using async updates for parts of the UI that are less important. Anyway, it looks like there are going a few moving parts to take into account.

@jsnajdr
Copy link
Member

jsnajdr commented Dec 20, 2021

using the experimental APIs that are present in React 17 (throwing promises) but I'm not clear yet whether React 18 will make changes

React Query in suspense mode throws promises, too. I think the promise-throwing is going to stay, as it's a rather fundamental aspect of suspense.

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.

There are two ways how useSuspenseSelect can behave, with subtle differences. After calling this:

const c = useSuspenseSelect( ( select ) => {
  const a = select( storeA ).get();
  const b = select( storeB ).get();
  return op( a, b );
} );

The current implementation will get() from both stores, with a potentially being an unresolved null, passes that null to op, and only after the callback finishes, proceeds to ignore the result and throw/suspend.

An alternative, imo slightly better impl would be to throw right out of the select( storeA ).get() call, never proceeding beyond that unresolved line. The author of the callback has a guarantee that select( storeA ).get() always returns a resolved value, not some null, and avoid a lot of null checks.

Another advantage of the alternative is that the "useSuspenseSelect operation is distributive", which is a fancy way of saying that:

const a = useSuspenseSelect( ( select ) => select( storeA ).get() );
const b = useSuspenseSelect( ( select ) => select( storeB ).get() );
const c = op( a, b );

Does this behave exactly the same way as the single unified useSuspenseSelect call above? One implementation guarantees that, the other doesn't.

After all, it's quite common in Gutenberg to select something from one store, like some entity kind or ID, and then feed that value into another select. But if the first select is unresolved, the second select doesn't even know what to wait for?

const state = store.getState();
if ( resolversCache.isRunning( selectorName, args ) ) {
registry.__unstableSuspend(
registry.resolveSelect(
Copy link
Member

Choose a reason for hiding this comment

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

This looks like potential infinite recursion? Calling a selector starts to resolve it, calling the resolver, then the resolver calls the selector again, triggering another resolution again? Maybe it's all carefully orchestrated so that the recursion always ends quickly, just flagging this as something that's worth a thorough review and testing.

@youknowriad
Copy link
Contributor Author

@jsnajdr Good point that said, I'm not sure yet about the perf implication here.

After all, it's quite common in Gutenberg to select something from one store, like some entity kind or ID, and then feed that value into another select. But if the first select is unresolved, the second select doesn't even know what to wait for?

I think this is true but since suspense is not supported yet, you could argue that the fallback is probably already handled anyway. It can be a nice way for the future to always assume that these things are resolved but is it a good thing?

From a DevX perspective, it's definitely a good thing but I fear the waterfall effect if consecutive calls don't share any dependency. I think this is also probably something common.

So there are downsides for both 🤷

@jsnajdr
Copy link
Member

jsnajdr commented Dec 21, 2021

It can be a nice way for the future to always assume that these things are resolved but is it a good thing?

It's not only a good thing, but it's exactly how suspense code should be written. When writing a suspense component, you call data-fetching functions while pretending that everything is synchronous and assuming that the fetching function always returns the data immediately:

function Post( { postId } ) {
  const post = fetchPost( postId );
  return <div><h1>{ post.title }</h1><p>{ post.excerpt }</p></div>;
}

There's no checking post for null or fetchPost returning an { isLoading, data } object where { isLoading: true, data: null } is a possible value that the component must know how to handle. That's what traditional data fetching APIs do, but suspense does things differently.

If the fetchPost data are not loaded yet, the function throws a promise telling React when to try again. If there is an error, fetchPost will throw (instead of returning some { error, data: null } state). These conditions are handled by a parent component (suspense fallback, error boundary), not by the component itself. The actual component ever implements only the "everything's fully loaded" case.

So, it only makes sense that the useSuspenseSelect callback should be written in the spirit of suspense, too, pretending that everything is synchronous and that selects always return loaded data. Because if we don't that, can we really claim we implemented suspense support? Yeah we've gone through the motions and checked off a checkbox, but the spirit is missing, we're still writing our components the traditional way, handling intermediate states ourselves instead of delegating them up the render tree.

@jsnajdr
Copy link
Member

jsnajdr commented Mar 29, 2022

Did a rebase onto latest trunk and resolved conflicts.

@jsnajdr jsnajdr force-pushed the add/suspense-support branch from 1906125 to 52d0104 Compare March 31, 2022 10:10
@jsnajdr
Copy link
Member

jsnajdr commented Mar 31, 2022

@youknowriad I reimplemented the useSuspenseSelect hook in a way that I think is correct: throwing immediately from any select.* call that's not resolved. Then you can write code that pretends to be completely synchronous, like:

useSuspenseSelect( ( select ) => {
  const id = select( 'store' ).getId();
  const post = select( 'store' ).getPost( id );
  const author = select( 'store' ).getAuthor( post.author );
  return { author };
}, [] );

Here each of the selectors, even though it's async, guarantees to always return a valid id or post or author. It will immediately throw if it can't return the final value.

I left in place your experimental usages, like the "Very Ugly Loading State" placeholder in Site Editor. You can use the hook and the <Suspense> component to implement the behavior you want.

It's not possible to detect in the useSuspenseSelect hook if there is a Suspense component in the parent chain and modify behavior. If we wanted that, we'd have to implement our own GutenSuspense component that additionally includes a context provider that passes down a isSuspense flag. Similar to how useAsyncMode works.

But I don't think it's a good idea: the difference between useSelect and useSuspenseSelect is not just flipping a flag and doing everything as before. With suspense, the entire programming philosophy is different: we pretend to write synchronous code where there are no intermediate states like null return values or isResolving checks. These are completely hidden from the programmer.

What I'll continue to work on now is a unit test suite that tests various scenarios. And I also want to modify behavior a bit in case when resolution fails for some selectors. In that case the correct thing to do is to read the resolution error and throw it. I.e., useSuspenseSelect can throw normal errors in case of errors, supposed to be caught by an error boundary, or throw promises, caught by Suspense component.

@youknowriad
Copy link
Contributor Author

youknowriad commented Mar 31, 2022

Thanks for the update.

But I don't think it's a good idea: the difference between useSelect and useSuspenseSelect is not just flipping a flag and doing everything as before. With suspense, the entire programming philosophy is different: we pretend to write synchronous code where there are no intermediate states like null return values or isResolving checks. These are completely hidden from the programmer.

The reasoning makes sense, I just want us to avoid breaking changes though for folks as we introduce "suspense" usage in blocks. We need to add the provider for our core block editors and maybe figure out a way to add it automatically to third-party block editors as well

@jsnajdr
Copy link
Member

jsnajdr commented Mar 31, 2022

I just want us to avoid breaking changes though for folks as we introduce "suspense" usage in blocks.

What could be good news is that you can safely move from useSelect to useSuspenseSelect, but not in the other direction.

With useSelect, selectors can return null or other unresolved values, and meta-selectors like isResolving can return true. That means the mapSelect function should check for these values. But when run in useSuspenseSelect, possible return values are more restricted, and the existing checks will have predictable results.

@jsnajdr
Copy link
Member

jsnajdr commented Apr 1, 2022

What I'll continue to work on now is a unit test suite that tests various scenarios. And I also want to modify behavior a bit in case when resolution fails for some selectors.

Done. There is a suite of unit tests, and I indeed had to modify the behavior on resolution failure in order to trigger an error boundary in one of the tests.

The @wordpress/data part of the PR is now ready for review and merging. I don't know what to do about the experimental usages of the hook across the codebase.

@jsnajdr jsnajdr marked this pull request as ready for review April 1, 2022 11:47
@jsnajdr jsnajdr requested a review from ajitbohra as a code owner April 1, 2022 11:47
@jsnajdr
Copy link
Member

jsnajdr commented Apr 26, 2022

I rebased this PR onto the latest trunk and removed Riad's unfinished experiments. Now this PR just adds useSuspenseSelect, with tests, and nothing more. I believe it's ready to be merged.

It's unfortunate that there's a lot of copy&paste from useSelect, but I plan to address that by factoring useSelect into several subhooks, like by finishing my own #40093 and @Inwerpsel's #40201.

}

return mapOutput;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

How different is the code here from the "raw" useSelect? Asking to see if we can share the same hook (or at least same base implementation) or if it's completely different?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nevermind, I just saw your comment above :P

Copy link
Member

Choose a reason for hiding this comment

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

The biggest difference, not easy to wrap in some if ( suspenseMode ) conditions, is how errors thrown by mapSelect are handled. useSelect kind of doesn't know what to do with them, it logs them to console, and otherwise tries to ignore them: it returns the previous (successful) mapSelect result, and hopes maybe on the next call the error will go away.

useSuspenseSelect simply rethrows the error, and lets suspense or error boundary to catch it.

@youknowriad
Copy link
Contributor Author

youknowriad commented Apr 26, 2022

What would be the plan for the next steps to implement this in site editor, patterns previews and such? Would be cool to improve the loading state for these.

@jsnajdr
Copy link
Member

jsnajdr commented Apr 26, 2022

What would be the plan for the next steps to implement this in site editor, patterns previews and such?

I don't know, I thought the plan was that you will try to implement the loading states 🙂

@youknowriad
Copy link
Contributor Author

I thought the plan was that you will try to implement the loading states

haha :) that's fine. I think I won't be able to get to it soon but we'll see.

@youknowriad
Copy link
Contributor Author

I can't approve my own PR, so feel free to approve and merge :)

@jsnajdr jsnajdr force-pushed the add/suspense-support branch from 8107b90 to f25f226 Compare April 28, 2022 08:10
@jsnajdr jsnajdr force-pushed the add/suspense-support branch from f25f226 to 23ecd56 Compare May 9, 2022 13:51
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.

Let's ship this 👍

@jsnajdr jsnajdr merged commit 1730fad into trunk May 9, 2022
@jsnajdr jsnajdr deleted the add/suspense-support branch May 9, 2022 18:07
@github-actions github-actions bot added this to the Gutenberg 13.3 milestone May 9, 2022
@nerrad
Copy link
Contributor

nerrad commented Jul 31, 2022

It looks like this was never added to the @wordpress/data changelog. I was just trying to find out what @wordpress/data package update useSuspenseSelect was released in. Given this is a significant new API update, should this be retroactively added to the changelog?

@mburridge
Copy link
Contributor

Added Needs Dev Note label in case a dev note is needed for 6.1 release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Framework Issues related to broader framework topics, especially as it relates to javascript Needs Dev Note Requires a developer note for a major WordPress release cycle [Package] Data /packages/data [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants