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

core-data: use controls from data-controls in favor of the package-local ones #25235

Merged
merged 1 commit into from
Sep 14, 2020

Conversation

jsnajdr
Copy link
Member

@jsnajdr jsnajdr commented Sep 11, 2020

The generic apiFetch, select and dispatch controls are frequently duplicated in various stores. This patch fixes that for the core-data package.

How to test:

  • check that both unit tests and e2e tests pass. As the core-data store is used at a lot of places, it's hard to point at any single feature.

Note:
I think the naming of the select controls in the data-controls package is very unfortunate:

  • select doesn't do the synchronous select (return whatever is in the store right now) that we all know from Redux or withSelect or useSelect, but does the promise-returning resolution based on __experimentalResolveSelect. This control should really be called resolveSelect.
  • __unstableSyncSelect has an unwieldy name and should be called select or at least syncSelect. I don't see anything that should be unstable or experimental about this control. It's the most non-controversial and straightforward select I can think of 🙂

@youknowriad @nerrad Is it possible to fix that naming, or are we stuck with it forever, for back-compat reasons?

It also seems to me that __experimentalResolveSelect should be graduated from the experimental state. It's being used in many async workflows that do several async requests sequentially and they depend on each other. Example: fetch info for postType and then use its restBase to fetch autosaves for it.

Another thing that would deserve first class support is calling a selector while skipping side-effects: something that __experimentalGetEntityRecordNoResolver does. That could be called peekSelect.

Our selectors and their resolvers are conceptually very similar to selectors in Recoil. There, every "node" has API like:

  • get(): return the value if resolved or throw a promise if unresolved (this is optimized for Suspense, but conceptually the same thing as our resolveSelect)
  • getLoadable(): return a { value, state } object where state can be one of loading, hasValue, hasError. Similar to synchronous select. Triggers resolution.
  • peekLoadable(): same as getLoadable but doesn't trigger resolution
  • invalidate(): drop the cached resolved value -- next get() will trigger new resolution.

…cal ones

The generic `apiFetch`, `select` and `dispatch` controls are frequently duplicated
in various stores. This patch fixes that for the `core-data` package.
@jsnajdr jsnajdr added [Type] Code Quality Issues or PRs that relate to code quality [Package] Core data /packages/core-data [Package] Data Controls /packages/data-controls labels Sep 11, 2020
@jsnajdr jsnajdr self-assigned this Sep 11, 2020
@github-actions
Copy link

Size Change: -191 B (0%)

Total Size: 1.2 MB

Filename Size Change
build/autop/index.js 2.82 kB -1 B
build/block-editor/index.js 128 kB +5 B (0%)
build/block-library/index.js 139 kB +1 B
build/block-serialization-default-parser/index.js 1.88 kB -1 B
build/blocks/index.js 47.8 kB -1 B
build/components/index.js 200 kB +2 B (0%)
build/compose/index.js 9.67 kB -5 B (0%)
build/core-data/index.js 12.2 kB -171 B (1%)
build/data/index.js 8.54 kB -2 B (0%)
build/deprecated/index.js 771 B -1 B
build/edit-navigation/index.js 10.7 kB -2 B (0%)
build/edit-post/index.js 305 kB -4 B (0%)
build/edit-site/index.js 19.3 kB -3 B (0%)
build/edit-widgets/index.js 12.2 kB -1 B
build/editor/index.js 45.6 kB -6 B (0%)
build/element/index.js 4.65 kB +1 B
build/format-library/index.js 7.71 kB -1 B
build/keyboard-shortcuts/index.js 2.52 kB -1 B
build/plugins/index.js 2.56 kB -1 B
build/rich-text/index.js 13.9 kB +1 B
build/server-side-render/index.js 2.77 kB +2 B (0%)
build/viewport/index.js 1.85 kB -2 B (0%)
ℹ️ View Unchanged
Filename Size Change
build/a11y/index.js 1.14 kB 0 B
build/annotations/index.js 3.67 kB 0 B
build/api-fetch/index.js 3.41 kB 0 B
build/blob/index.js 620 B 0 B
build/block-directory/index.js 8.54 kB 0 B
build/block-directory/style-rtl.css 953 B 0 B
build/block-directory/style.css 952 B 0 B
build/block-editor/style-rtl.css 11.1 kB 0 B
build/block-editor/style.css 11.1 kB 0 B
build/block-library/editor-rtl.css 8.68 kB 0 B
build/block-library/editor.css 8.68 kB 0 B
build/block-library/style-rtl.css 7.59 kB 0 B
build/block-library/style.css 7.58 kB 0 B
build/block-library/theme-rtl.css 754 B 0 B
build/block-library/theme.css 754 B 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/data-controls/index.js 1.29 kB 0 B
build/date/index.js 31.9 kB 0 B
build/dom-ready/index.js 568 B 0 B
build/dom/index.js 4.47 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/style-rtl.css 6.26 kB 0 B
build/edit-post/style.css 6.25 kB 0 B
build/edit-site/style-rtl.css 3.06 kB 0 B
build/edit-site/style.css 3.06 kB 0 B
build/edit-widgets/style-rtl.css 2.55 kB 0 B
build/edit-widgets/style.css 2.55 kB 0 B
build/editor/editor-styles-rtl.css 492 B 0 B
build/editor/editor-styles.css 493 B 0 B
build/editor/style-rtl.css 3.81 kB 0 B
build/editor/style.css 3.81 kB 0 B
build/escape-html/index.js 733 B 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 2.13 kB 0 B
build/html-entities/index.js 621 B 0 B
build/i18n/index.js 3.56 kB 0 B
build/is-shallow-equal/index.js 710 B 0 B
build/keycodes/index.js 1.94 kB 0 B
build/list-reusable-blocks/index.js 3.12 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.32 kB 0 B
build/notices/index.js 1.79 kB 0 B
build/nux/index.js 3.4 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.41 kB 0 B
build/priority-queue/index.js 789 B 0 B
build/redux-routine/index.js 2.85 kB 0 B
build/shortcode/index.js 1.7 kB 0 B
build/token-list/index.js 1.27 kB 0 B
build/url/index.js 4.06 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.17 kB 0 B

compressed-size-action

@nerrad
Copy link
Contributor

nerrad commented Sep 11, 2020

I agree naming is unfortunate. Such is what happens as changes evolve over time. I don't think we can change the naming right now for back-compat reasons but happy to be wrong if Riad has ideas.

I'm okay with the other suggestions regarding graduating unstable/experimental APIs but I'd defer to Riad for the final call here.

Another thing that would deserve first class support is calling a selector while skipping side-effects: something that __experimentalGetEntityRecordNoResolver does. That could be called peekSelect.

That does sound like it could be good for the use-case where consumers want the current state without triggering any potential resolvers. I think peekSelect sounds like a good name.

@youknowriad
Copy link
Contributor

Same here, I don't think we can rename now the stable ones. I'm ok with making the experimental APIs stable here.

Maybe we want to wait before introducing peekSelect (wait for a concrete use-case in Gutenberg)

@jsnajdr
Copy link
Member Author

jsnajdr commented Sep 14, 2020

Same here, I don't think we can rename now the stable ones.

One approach that fixes a big part of the damage while keeping back compat:

  • keep the existing select control, with the resolveSelect behavior, but mark it as deprecated to discourage new uses
  • introduce controls named syncSelect (already there), resolveSelect and peekSelect, where the "select" word is always prefixed with something that makes the meaning clear.

I'm ok with making the experimental APIs stable here.

OK, I can do it in another PR. Keeping this one focused on introducing data-controls in core-data and removing the core-data home-grown ones.

Maybe we want to wait before introducing peekSelect (wait for a concrete use-case in Gutenberg)

I definitely agree with not doing it in this PR, if that's what you mean by "waiting". Regarding the concrete use-case, I believe it's already there in __experimentalGetEntityRecordNoResolver. But we can wait until more use cases emerge, this is not urgent at all.

@youknowriad
Copy link
Contributor

I'm not sure I like the renaming back-compat approach because I like the parallel between select and dispatch. In fact with this parallel, I wonder if the current select behavior (triggering the resolve as well) is the correct approach. I think we can probably introduce both syncSelect and resolveSelect for explicitness but I'm not sure about the deprecation.

@jsnajdr
Copy link
Member Author

jsnajdr commented Sep 14, 2020

In fact with this parallel, I wonder if the current select behavior (triggering the resolve as well) is the correct approach.

From a Redux purist perspective, triggering the resolver as a side-effect of select has been "incorrect" from the very beginning. A Redux selector should compute and return some value from state and do nothing else.

Triggering the resolver is also problematic because it's done during the render phase. It's incompatible with React async rendering: React can discard the render results and never commit them, and yet the side effect will be triggered. It also causes the new "cannot update component while rendering component" React warning and workarounds were needed in #21289. I believe this is an issue with the current useSelect and withSelect implementation, rather than a "fundamental" problem, and can be fixed by postponing the resolver call to an useEffect callback.

Aside from Redux, the concept of "selector with a resolver" is OK, and libraries like Recoil use it: useRecoilValue( entitySelector( postId ) ) would both return the current value and trigger the resolver. Their implementation does it correctly though, triggering the resolver as effect, after the render result is committed.

My main concern with the naming is that the names of the controls don't correspond to the store methods they are wrappers for: the select( 'core', ... ) control doesn't synchronously select, as registry.select( 'core', ... ) would do.

@jsnajdr jsnajdr merged commit 2591d58 into master Sep 14, 2020
@jsnajdr jsnajdr deleted the use/data-controls-in-core-data branch September 14, 2020 09:52
@github-actions github-actions bot added this to the Gutenberg 9.0 milestone Sep 14, 2020
@youknowriad
Copy link
Contributor

I believe this is an issue with the current useSelect and withSelect implementation, rather than a "fundamental" problem, and can be fixed by postponing the resolver call to an useEffect callback.

The API is meant to be "React-agnostic". If we do this change, it means we're changing the semantics of registry.select... probably a BC incompatible change.

@jsnajdr
Copy link
Member Author

jsnajdr commented Sep 14, 2020

If we do this change, it means we're changing the semantics of registry.select... probably a BC incompatible change.

I think it's posible to keep registry.select as it is and improve only the React bindings. What we want to change is the scheduling of the resolver calls, nothing else.

For regular registry.select, the resolvers used to be scheduled synchronously, today they are scheduled with setTimeout( 0 ).

For React bindings, we want to batch the resolver calls and fire them all later in a useEffect callback. Similarly to how React batches setState calls while in DOM event handler callback.

This is a possible implementation of useSelect that calls the selectors with a custom ReactScheduler:

const useSelect = ( mapSelect ) => {
  const registry = useRegistry();
  const { scheduler, select } = registry.selectWithScheduler( ReactScheduler );
  const mapOutput = mapSelect( select );
  useEffect( () => {
    scheduler.flush();
  } );
  return mapOutput;
}

The ReactScheduler batches the resolver calls and they are executed only later in useEffect.

I'm not sure if this can ever really work, especially in a large React tree where components interact, but it looks like it can be done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Core data /packages/core-data [Package] Data Controls /packages/data-controls [Type] Code Quality Issues or PRs that relate to code quality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants