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

Migrate the 'editor' package to builtin data controls #25990

Merged
merged 2 commits into from
Oct 20, 2020

Conversation

jsnajdr
Copy link
Member

@jsnajdr jsnajdr commented Oct 9, 2020

Migrates the editor package to the builtin data controls introduced in #25362.

Builds on top of #25987 that improves some of the affected tests.

While reviewing and testing, particular attention should go to the choices between the sync select and async resolveSelect control. The only selector where we need to wait for resolution is select( 'core' ).getPostType(). Everything else is sync. All selects from core/editor in particular are always sync, because the store doesn't have any resolvers.

@jsnajdr jsnajdr self-assigned this Oct 9, 2020
@jsnajdr jsnajdr added [Package] Editor /packages/editor [Type] Code Quality Issues or PRs that relate to code quality labels Oct 9, 2020
@github-actions
Copy link

github-actions bot commented Oct 9, 2020

Size Change: +12 B (0%)

Total Size: 1.19 MB

Filename Size Change
build/block-directory/index.js 8.55 kB +1 B
build/block-editor/index.js 130 kB -1 B
build/block-library/index.js 145 kB +1 B
build/blocks/index.js 47.6 kB +1 B
build/components/index.js 169 kB +4 B (0%)
build/core-data/index.js 12 kB +1 B
build/edit-navigation/index.js 10.6 kB +1 B
build/edit-site/index.js 21.3 kB -2 B (0%)
build/edit-widgets/index.js 21.2 kB -3 B (0%)
build/editor/index.js 45.5 kB +7 B (0%)
build/list-reusable-blocks/index.js 3.02 kB +1 B
build/plugins/index.js 2.44 kB +1 B
ℹ️ View Unchanged
Filename Size Change
build/a11y/index.js 1.14 kB 0 B
build/annotations/index.js 3.52 kB 0 B
build/api-fetch/index.js 3.35 kB 0 B
build/autop/index.js 2.72 kB 0 B
build/blob/index.js 668 B 0 B
build/block-directory/style-rtl.css 943 B 0 B
build/block-directory/style.css 942 B 0 B
build/block-editor/style-rtl.css 10.9 kB 0 B
build/block-editor/style.css 10.9 kB 0 B
build/block-library/editor-rtl.css 8.65 kB 0 B
build/block-library/editor.css 8.65 kB 0 B
build/block-library/style-rtl.css 7.67 kB 0 B
build/block-library/style.css 7.66 kB 0 B
build/block-library/theme-rtl.css 741 B 0 B
build/block-library/theme.css 741 B 0 B
build/block-serialization-default-parser/index.js 1.78 kB 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/components/style-rtl.css 15.5 kB 0 B
build/components/style.css 15.5 kB 0 B
build/compose/index.js 9.43 kB 0 B
build/data-controls/index.js 685 B 0 B
build/data/index.js 8.6 kB 0 B
build/date/index.js 31.9 kB 0 B
build/deprecated/index.js 772 B 0 B
build/dom-ready/index.js 568 B 0 B
build/dom/index.js 4.42 kB 0 B
build/edit-navigation/style-rtl.css 868 B 0 B
build/edit-navigation/style.css 871 B 0 B
build/edit-post/index.js 306 kB 0 B
build/edit-post/style-rtl.css 6.29 kB 0 B
build/edit-post/style.css 6.28 kB 0 B
build/edit-site/style-rtl.css 3.8 kB 0 B
build/edit-site/style.css 3.81 kB 0 B
build/edit-widgets/style-rtl.css 3.03 kB 0 B
build/edit-widgets/style.css 3.03 kB 0 B
build/editor/editor-styles-rtl.css 480 B 0 B
build/editor/editor-styles.css 482 B 0 B
build/editor/style-rtl.css 3.85 kB 0 B
build/editor/style.css 3.84 kB 0 B
build/element/index.js 4.45 kB 0 B
build/escape-html/index.js 734 B 0 B
build/format-library/index.js 7.49 kB 0 B
build/format-library/style-rtl.css 547 B 0 B
build/format-library/style.css 548 B 0 B
build/hooks/index.js 1.74 kB 0 B
build/html-entities/index.js 621 B 0 B
build/i18n/index.js 3.54 kB 0 B
build/is-shallow-equal/index.js 710 B 0 B
build/keyboard-shortcuts/index.js 2.39 kB 0 B
build/keycodes/index.js 1.85 kB 0 B
build/list-reusable-blocks/style-rtl.css 476 B 0 B
build/list-reusable-blocks/style.css 476 B 0 B
build/media-utils/index.js 5.13 kB 0 B
build/notices/index.js 1.69 kB 0 B
build/nux/index.js 3.27 kB 0 B
build/nux/style-rtl.css 671 B 0 B
build/nux/style.css 668 B 0 B
build/primitives/index.js 1.34 kB 0 B
build/priority-queue/index.js 790 B 0 B
build/redux-routine/index.js 2.85 kB 0 B
build/rich-text/index.js 13 kB 0 B
build/server-side-render/index.js 2.6 kB 0 B
build/shortcode/index.js 1.7 kB 0 B
build/token-list/index.js 1.24 kB 0 B
build/url/index.js 4.06 kB 0 B
build/viewport/index.js 1.74 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.17 kB 0 B

compressed-size-action

@youknowriad
Copy link
Contributor

While reviewing and testing, particular attention should go to the choices between the sync select and async resolveSelect control. The only selector where we need to wait for resolution is select( 'core' ).getPostType(). Everything else is sync. All selects from core/editor in particular are always sync, because the store doesn't have any resolvers.

It feels like it shouldn't be the consumer's responsibility to think about this. Why not always use resolveSelect and make sure that it resolves directly if the selector has no resolver.

@jsnajdr
Copy link
Member Author

jsnajdr commented Oct 12, 2020

Why not always use resolveSelect and make sure that it resolves directly if the selector has no resolver.

That's possible -- resolveSelect works the same way as select (synchronously) if the selector has no resolver.

But I feel the programmer should be aware of the difference between these two types of selectors, as they are conceptually two different things.

Selector without a resolver is basically a glorified variable in memory that can be read and assigned to, in a React-compatible way.

Selector with a resolver means there is a remote request that needs to be resolved asynchronously. It can fail and the result it stored locally in a cache (that all what the local reducer and store really is -- cache for these requests)

In the future, I think both types of selectors will be accessed through two distinct APIs, and each of them will feel natural for the purpose. At this moment, the difference between select and resolveSelect looks confusing, and I hope it will cease to be.

@youknowriad
Copy link
Contributor

In the future, I think both types of selectors will be accessed through two distinct APIs, and each of them will feel natural for the purpose. At this moment, the difference between select and resolveSelect looks confusing, and I hope it will cease to be.

I think that's a big shift for the data package. It was modeled from the start that components declare their data needs (select) and don't care whether it's sync or async, it's an implementation detail the component user should rarely think about. (granted it's not always true). Based from this principle, it seems coherent to always use resolve select in controls IMO.

Now, whether that assumption/principle is to be rediscussed/rethought is a big decision.

@jsnajdr jsnajdr force-pushed the migrate/editor-builtin-controls branch from d827f3d to c838bdd Compare October 12, 2020 14:36
@jsnajdr
Copy link
Member Author

jsnajdr commented Oct 12, 2020

Rebased and resolved conflict with #20149 (stabilizing the local autosave API)

@jsnajdr
Copy link
Member Author

jsnajdr commented Oct 13, 2020

It was modeled from the start that components declare their data needs (select) and don't care whether it's sync or async, it's an implementation detail the component user should rarely think about.

Maybe I misunderstand something, but a React component author must be aware about the async nature of a selector with resolver. It's not possible to write code that works otherwise. As a random example, consider the FullscreenModeClose component that requests the site icon.

The component needs to handle the case where the selector returns null (because it's resolving), interpret the meaning of that return value correctly (site has no icon vs. site icon is fetching) and use the isResolving meta-selector to check the resolving state. It should also be able to handle failed resolutions, which it doesn't do now.

Compare that to how the select( 'core/edit-post' ).isFeatureActive( 'fullscreenMode' ) selector is used in the same component. That value is completely synchronous, always reliably true or false. Similarly, dispatching the toggleFeature( 'fullscreenMode' ) action is also synchronous, the value is changed immediately, and you can read the new value on the next line and it will be updated.

Based from this principle, it seems coherent to always use resolve select in controls IMO.

It's true that when we use a selector-with-resolver in a control, we almost always want the resolved value. The control typically implements some async flow (do this and then do that), and the flow is ran only once -- there's no opportunity to "react" to store changes and rerun it with updated values.

But I'm afraid that always using resolveSelect and not being aware about sync/async can lead to race conditions and bugs.

Consider the fix that @epiqueras did in #21839 to fix race conditions in the resetEditorBlocks control. After one of the used selector got a resolver it previously didn't have, a synchronous function suddenly became asynchronous. Its code started to run in parallel with other code, and the race condition lead to data corruption. That's where syncSelect was introduced to make the control synchronous again.

That's a typical example that the programmer had better be aware about sync/async even when writing controls.

Now, whether that assumption/principle is to be rediscussed/rethought is a big decision.

First of all, I believe this assumption/principle simply doesn't hold in the data package as it is today. The user of the API needs to be aware of the sync/async nature of things if they want to write reliable code.

Second, I don't think that the ambitions I described are that radical 🙂 An example of one thing I'd like to see changed: the FullscreenModeClose component gets the site icon URL with code like this:

const { siteIconUrl, isRequestingSiteIcon } = useSelect( ( select ) => {
  const { getEntityRecord, isResolving } = select( 'core' );
  return {
    siteIconUrl: getEntityRecord( 'root', '__unstableBase', undefined )?.site_icon_url,
    isRequestingSiteIcon: isResolving( 'getEntityRecord', [ 'root', '__unstableBase', undefined ] ),
  };
}, [] );

This code is a bit unwieldy and repetitive, and I'd like to arrive at some much more elegant API to express the same thing. That's shouldn't need any radical rewrite -- just some incremental refactorings and then maybe one new hook that builds on top of useSelect.

Copy link
Contributor

@youknowriad youknowriad 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 personally not convinced and still think there's value in abstracting the "async" behavior or always assuming it's async but it's not that important and we shouldn't block this PR.

@jsnajdr jsnajdr merged commit 1d6b8dc into master Oct 20, 2020
@jsnajdr jsnajdr deleted the migrate/editor-builtin-controls branch October 20, 2020 07:11
@github-actions github-actions bot added this to the Gutenberg 9.3 milestone Oct 20, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Editor /packages/editor [Type] Code Quality Issues or PRs that relate to code quality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants