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 thunk actions to replace rungen and controls #27276

Merged
merged 2 commits into from
Jan 28, 2021

Conversation

adamziel
Copy link
Contributor

@adamziel adamziel commented Nov 25, 2020

Description

As the future of @wordpress/stan is uncertain (#27109), I thought I'd experiment with removing rungen dependency. This PR refactors a bunch of generator-based actions leveraging controls into regular asynchronous functions without dependency on controls. This is not time-sensitive and could be made obsolete by stan, but I just wanted to set things in motion here.

A bunch of unit tests are going to fail simply because I did not adjust them. For now I'm more interested in e2e tests to see if such implementation breaks anything for the user.

Refactored use-cases:

Using selectors in actions

Before: yield select( 'core', 'getEntityRecord', 'root', 'post', id );
After: select( 'core' ).getEntityRecord( 'root', 'post', id );

Dispatching store-bound actions

Before: yield dispatch( 'core', 'receiveEntityRecord', 'root', 'post', data );
After: await dispatch( 'core' ).receiveEntityRecord( 'root', 'post', data );

Dispatching inline actions

Before: yield { type: 'ACTION_NAME' }
After: await yieldAction( { type: 'ACTION_NAME' } )

I'm not overly enthusiastic about this API, but I don't have any better ideas, other than maybe exposing every tiny action as a public API.

Deferring to generatorfunction (for compat)

Before: yield* generatorFunction()
After: await yieldAction( generatorFunction() )
A better way: await dispatch( 'core' ).generatorAction();

Using controls

Just do the task inline, e.g.:

Before:

import { apiFetch } from '@wordpress/data-controls';
yield apiFetch( request );

After:

import { apiFetch } from '@wordpress/api-fetch';
await apiFetch( request );

Notes

  • Not all core actions and resolvers were refactored in this PR - this shows how generators and async are interoperable, which is important in between landing PRs.
  • This PR should really do a better job of not using string as store keys (wink wink @gziolo :-))
  • Refactoring all stores will make it possible to remove reduxRoutime middleware and all the code related to rungen, controls, generators, etc.
  • At the moment, actions have access to dispatch, select, and yieldAction via registryArgs that's added via store creator. This should be okay for the first version, but ideally we wouldn't expose these functions globally.
  • My largest concern about this approach is the fact that const data = yield anotherAction(); could be entirely synchronous while const data await = await dispatch( 'store' ).anotherAction(); will always be asynchronous. I am not sure if that's a problem though. First, we have locks API for when that matters. Second, yield-based dispatch could suddenly go asynchronous if anything in the call tree uses an async control. In other words, refactoring generator-based action A could have a ripple effect in seemingly unrelated action D that calls A via C and B.

Related #26849 #27109
cc @jsnajdr @kevin940726 @noisysocks @draganescu @youknowriad @ellatrix

@github-actions
Copy link

github-actions bot commented Nov 25, 2020

Size Change: -156 B (0%)

Total Size: 1.4 MB

Filename Size Change
build/block-library/index.js 143 kB +1 B (0%)
build/components/index.js 292 kB +2 B (0%)
build/data/index.js 8.81 kB -158 B (-2%)
build/shortcode/index.js 1.7 kB -1 B (0%)
ℹ️ View Unchanged
Filename Size Change
build/a11y/index.js 1.14 kB 0 B
build/annotations/index.js 3.77 kB 0 B
build/api-fetch/index.js 3.4 kB 0 B
build/autop/index.js 2.82 kB 0 B
build/blob/index.js 665 B 0 B
build/block-directory/index.js 9.08 kB 0 B
build/block-directory/style-rtl.css 1.01 kB 0 B
build/block-directory/style.css 1.01 kB 0 B
build/block-editor/index.js 122 kB 0 B
build/block-editor/style-rtl.css 11.9 kB 0 B
build/block-editor/style.css 11.9 kB 0 B
build/block-library/blocks/archives/editor-rtl.css 196 B 0 B
build/block-library/blocks/archives/editor.css 196 B 0 B
build/block-library/blocks/audio/editor-rtl.css 194 B 0 B
build/block-library/blocks/audio/editor.css 194 B 0 B
build/block-library/blocks/audio/style-rtl.css 225 B 0 B
build/block-library/blocks/audio/style.css 225 B 0 B
build/block-library/blocks/block/editor-rtl.css 283 B 0 B
build/block-library/blocks/block/editor.css 283 B 0 B
build/block-library/blocks/button/editor-rtl.css 576 B 0 B
build/block-library/blocks/button/editor.css 577 B 0 B
build/block-library/blocks/button/style-rtl.css 552 B 0 B
build/block-library/blocks/button/style.css 552 B 0 B
build/block-library/blocks/buttons/editor-rtl.css 345 B 0 B
build/block-library/blocks/buttons/editor.css 346 B 0 B
build/block-library/blocks/buttons/style-rtl.css 419 B 0 B
build/block-library/blocks/buttons/style.css 419 B 0 B
build/block-library/blocks/calendar/style-rtl.css 319 B 0 B
build/block-library/blocks/calendar/style.css 319 B 0 B
build/block-library/blocks/categories/editor-rtl.css 210 B 0 B
build/block-library/blocks/categories/editor.css 209 B 0 B
build/block-library/blocks/categories/style-rtl.css 208 B 0 B
build/block-library/blocks/categories/style.css 208 B 0 B
build/block-library/blocks/code/style-rtl.css 216 B 0 B
build/block-library/blocks/code/style.css 216 B 0 B
build/block-library/blocks/columns/editor-rtl.css 300 B 0 B
build/block-library/blocks/columns/editor.css 299 B 0 B
build/block-library/blocks/columns/style-rtl.css 529 B 0 B
build/block-library/blocks/columns/style.css 528 B 0 B
build/block-library/blocks/cover/editor-rtl.css 524 B 0 B
build/block-library/blocks/cover/editor.css 522 B 0 B
build/block-library/blocks/cover/style-rtl.css 1.3 kB 0 B
build/block-library/blocks/cover/style.css 1.3 kB 0 B
build/block-library/blocks/embed/editor-rtl.css 594 B 0 B
build/block-library/blocks/embed/editor.css 595 B 0 B
build/block-library/blocks/embed/style-rtl.css 489 B 0 B
build/block-library/blocks/embed/style.css 489 B 0 B
build/block-library/blocks/file/editor-rtl.css 314 B 0 B
build/block-library/blocks/file/editor.css 313 B 0 B
build/block-library/blocks/file/style-rtl.css 352 B 0 B
build/block-library/blocks/file/style.css 352 B 0 B
build/block-library/blocks/freeform/editor-rtl.css 2.55 kB 0 B
build/block-library/blocks/freeform/editor.css 2.55 kB 0 B
build/block-library/blocks/gallery/editor-rtl.css 783 B 0 B
build/block-library/blocks/gallery/editor.css 783 B 0 B
build/block-library/blocks/gallery/style-rtl.css 1.17 kB 0 B
build/block-library/blocks/gallery/style.css 1.17 kB 0 B
build/block-library/blocks/group/editor-rtl.css 433 B 0 B
build/block-library/blocks/group/editor.css 432 B 0 B
build/block-library/blocks/group/style-rtl.css 190 B 0 B
build/block-library/blocks/group/style.css 190 B 0 B
build/block-library/blocks/heading/editor-rtl.css 248 B 0 B
build/block-library/blocks/heading/editor.css 248 B 0 B
build/block-library/blocks/heading/style-rtl.css 212 B 0 B
build/block-library/blocks/heading/style.css 212 B 0 B
build/block-library/blocks/html/editor-rtl.css 384 B 0 B
build/block-library/blocks/html/editor.css 385 B 0 B
build/block-library/blocks/image/editor-rtl.css 801 B 0 B
build/block-library/blocks/image/editor.css 800 B 0 B
build/block-library/blocks/image/style-rtl.css 569 B 0 B
build/block-library/blocks/image/style.css 570 B 0 B
build/block-library/blocks/latest-comments/editor-rtl.css 277 B 0 B
build/block-library/blocks/latest-comments/editor.css 275 B 0 B
build/block-library/blocks/latest-comments/style-rtl.css 382 B 0 B
build/block-library/blocks/latest-comments/style.css 382 B 0 B
build/block-library/blocks/latest-posts/editor-rtl.css 254 B 0 B
build/block-library/blocks/latest-posts/editor.css 254 B 0 B
build/block-library/blocks/latest-posts/style-rtl.css 634 B 0 B
build/block-library/blocks/latest-posts/style.css 634 B 0 B
build/block-library/blocks/list/editor-rtl.css 203 B 0 B
build/block-library/blocks/list/editor.css 203 B 0 B
build/block-library/blocks/list/style-rtl.css 201 B 0 B
build/block-library/blocks/list/style.css 201 B 0 B
build/block-library/blocks/media-text/editor-rtl.css 311 B 0 B
build/block-library/blocks/media-text/editor.css 311 B 0 B
build/block-library/blocks/media-text/style-rtl.css 642 B 0 B
build/block-library/blocks/media-text/style.css 640 B 0 B
build/block-library/blocks/more/editor-rtl.css 545 B 0 B
build/block-library/blocks/more/editor.css 545 B 0 B
build/block-library/blocks/navigation-link/editor-rtl.css 503 B 0 B
build/block-library/blocks/navigation-link/editor.css 504 B 0 B
build/block-library/blocks/navigation-link/style-rtl.css 805 B 0 B
build/block-library/blocks/navigation-link/style.css 803 B 0 B
build/block-library/blocks/navigation/editor-rtl.css 1.49 kB 0 B
build/block-library/blocks/navigation/editor.css 1.48 kB 0 B
build/block-library/blocks/navigation/style-rtl.css 289 B 0 B
build/block-library/blocks/navigation/style.css 289 B 0 B
build/block-library/blocks/nextpage/editor-rtl.css 507 B 0 B
build/block-library/blocks/nextpage/editor.css 507 B 0 B
build/block-library/blocks/paragraph/editor-rtl.css 236 B 0 B
build/block-library/blocks/paragraph/editor.css 236 B 0 B
build/block-library/blocks/paragraph/style-rtl.css 392 B 0 B
build/block-library/blocks/paragraph/style.css 392 B 0 B
build/block-library/blocks/post-author/editor-rtl.css 329 B 0 B
build/block-library/blocks/post-author/editor.css 329 B 0 B
build/block-library/blocks/post-author/style-rtl.css 303 B 0 B
build/block-library/blocks/post-author/style.css 303 B 0 B
build/block-library/blocks/post-comments-form/style-rtl.css 358 B 0 B
build/block-library/blocks/post-comments-form/style.css 358 B 0 B
build/block-library/blocks/post-content/editor-rtl.css 262 B 0 B
build/block-library/blocks/post-content/editor.css 262 B 0 B
build/block-library/blocks/post-excerpt/editor-rtl.css 206 B 0 B
build/block-library/blocks/post-excerpt/editor.css 206 B 0 B
build/block-library/blocks/post-featured-image/editor-rtl.css 453 B 0 B
build/block-library/blocks/post-featured-image/editor.css 453 B 0 B
build/block-library/blocks/post-featured-image/style-rtl.css 223 B 0 B
build/block-library/blocks/post-featured-image/style.css 223 B 0 B
build/block-library/blocks/preformatted/style-rtl.css 193 B 0 B
build/block-library/blocks/preformatted/style.css 193 B 0 B
build/block-library/blocks/pullquote/editor-rtl.css 304 B 0 B
build/block-library/blocks/pullquote/editor.css 304 B 0 B
build/block-library/blocks/pullquote/style-rtl.css 428 B 0 B
build/block-library/blocks/pullquote/style.css 428 B 0 B
build/block-library/blocks/query-loop/editor-rtl.css 217 B 0 B
build/block-library/blocks/query-loop/editor.css 216 B 0 B
build/block-library/blocks/query-loop/style-rtl.css 427 B 0 B
build/block-library/blocks/query-loop/style.css 429 B 0 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 243 B 0 B
build/block-library/blocks/query-pagination-numbers/editor.css 240 B 0 B
build/block-library/blocks/query-pagination/editor-rtl.css 390 B 0 B
build/block-library/blocks/query-pagination/editor.css 379 B 0 B
build/block-library/blocks/query-pagination/style-rtl.css 288 B 0 B
build/block-library/blocks/query-pagination/style.css 288 B 0 B
build/block-library/blocks/query/editor-rtl.css 279 B 0 B
build/block-library/blocks/query/editor.css 279 B 0 B
build/block-library/blocks/quote/editor-rtl.css 195 B 0 B
build/block-library/blocks/quote/editor.css 195 B 0 B
build/block-library/blocks/quote/style-rtl.css 284 B 0 B
build/block-library/blocks/quote/style.css 285 B 0 B
build/block-library/blocks/rss/editor-rtl.css 307 B 0 B
build/block-library/blocks/rss/editor.css 309 B 0 B
build/block-library/blocks/rss/style-rtl.css 394 B 0 B
build/block-library/blocks/rss/style.css 393 B 0 B
build/block-library/blocks/search/editor-rtl.css 285 B 0 B
build/block-library/blocks/search/editor.css 285 B 0 B
build/block-library/blocks/search/style-rtl.css 454 B 0 B
build/block-library/blocks/search/style.css 456 B 0 B
build/block-library/blocks/separator/editor-rtl.css 229 B 0 B
build/block-library/blocks/separator/editor.css 229 B 0 B
build/block-library/blocks/separator/style-rtl.css 352 B 0 B
build/block-library/blocks/separator/style.css 352 B 0 B
build/block-library/blocks/shortcode/editor-rtl.css 603 B 0 B
build/block-library/blocks/shortcode/editor.css 603 B 0 B
build/block-library/blocks/site-logo/editor-rtl.css 321 B 0 B
build/block-library/blocks/site-logo/editor.css 321 B 0 B
build/block-library/blocks/site-logo/style-rtl.css 238 B 0 B
build/block-library/blocks/site-logo/style.css 238 B 0 B
build/block-library/blocks/social-link/editor-rtl.css 283 B 0 B
build/block-library/blocks/social-link/editor.css 283 B 0 B
build/block-library/blocks/social-links/editor-rtl.css 811 B 0 B
build/block-library/blocks/social-links/editor.css 810 B 0 B
build/block-library/blocks/social-links/style-rtl.css 1.48 kB 0 B
build/block-library/blocks/social-links/style.css 1.48 kB 0 B
build/block-library/blocks/spacer/editor-rtl.css 416 B 0 B
build/block-library/blocks/spacer/editor.css 416 B 0 B
build/block-library/blocks/spacer/style-rtl.css 184 B 0 B
build/block-library/blocks/spacer/style.css 184 B 0 B
build/block-library/blocks/subhead/editor-rtl.css 223 B 0 B
build/block-library/blocks/subhead/editor.css 223 B 0 B
build/block-library/blocks/subhead/style-rtl.css 210 B 0 B
build/block-library/blocks/subhead/style.css 210 B 0 B
build/block-library/blocks/table/editor-rtl.css 593 B 0 B
build/block-library/blocks/table/editor.css 593 B 0 B
build/block-library/blocks/table/style-rtl.css 501 B 0 B
build/block-library/blocks/table/style.css 501 B 0 B
build/block-library/blocks/tag-cloud/editor-rtl.css 237 B 0 B
build/block-library/blocks/tag-cloud/editor.css 235 B 0 B
build/block-library/blocks/tag-cloud/style-rtl.css 221 B 0 B
build/block-library/blocks/tag-cloud/style.css 221 B 0 B
build/block-library/blocks/template-part/editor-rtl.css 794 B 0 B
build/block-library/blocks/template-part/editor.css 794 B 0 B
build/block-library/blocks/text-columns/editor-rtl.css 220 B 0 B
build/block-library/blocks/text-columns/editor.css 220 B 0 B
build/block-library/blocks/text-columns/style-rtl.css 283 B 0 B
build/block-library/blocks/text-columns/style.css 283 B 0 B
build/block-library/blocks/verse/editor-rtl.css 194 B 0 B
build/block-library/blocks/verse/editor.css 194 B 0 B
build/block-library/blocks/verse/style-rtl.css 215 B 0 B
build/block-library/blocks/verse/style.css 215 B 0 B
build/block-library/blocks/video/editor-rtl.css 617 B 0 B
build/block-library/blocks/video/editor.css 617 B 0 B
build/block-library/blocks/video/style-rtl.css 303 B 0 B
build/block-library/blocks/video/style.css 304 B 0 B
build/block-library/common-rtl.css 1.01 kB 0 B
build/block-library/common.css 1.01 kB 0 B
build/block-library/editor-rtl.css 9.2 kB 0 B
build/block-library/editor.css 9.18 kB 0 B
build/block-library/style-rtl.css 8.64 kB 0 B
build/block-library/style.css 8.65 kB 0 B
build/block-library/theme-rtl.css 860 B 0 B
build/block-library/theme.css 860 B 0 B
build/block-serialization-default-parser/index.js 1.88 kB 0 B
build/block-serialization-spec-parser/index.js 3.06 kB 0 B
build/blocks/index.js 48.2 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 11.2 kB 0 B
build/core-data/index.js 15.1 kB 0 B
build/data-controls/index.js 827 B 0 B
build/date/index.js 31.8 kB 0 B
build/deprecated/index.js 769 B 0 B
build/dom-ready/index.js 571 B 0 B
build/dom/index.js 4.94 kB 0 B
build/edit-navigation/index.js 11.1 kB 0 B
build/edit-navigation/style-rtl.css 938 B 0 B
build/edit-navigation/style.css 944 B 0 B
build/edit-post/index.js 306 kB 0 B
build/edit-post/style-rtl.css 6.51 kB 0 B
build/edit-post/style.css 6.5 kB 0 B
build/edit-site/index.js 24 kB 0 B
build/edit-site/style-rtl.css 4.01 kB 0 B
build/edit-site/style.css 4.01 kB 0 B
build/edit-widgets/index.js 23.5 kB 0 B
build/edit-widgets/style-rtl.css 3.17 kB 0 B
build/edit-widgets/style.css 3.18 kB 0 B
build/editor/editor-styles-rtl.css 543 B 0 B
build/editor/editor-styles.css 545 B 0 B
build/editor/index.js 41.8 kB 0 B
build/editor/style-rtl.css 3.89 kB 0 B
build/editor/style.css 3.89 kB 0 B
build/element/index.js 4.61 kB 0 B
build/escape-html/index.js 735 B 0 B
build/format-library/index.js 6.75 kB 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.27 kB 0 B
build/html-entities/index.js 622 B 0 B
build/i18n/index.js 3.56 kB 0 B
build/is-shallow-equal/index.js 699 B 0 B
build/keyboard-shortcuts/index.js 2.53 kB 0 B
build/keycodes/index.js 1.93 kB 0 B
build/list-reusable-blocks/index.js 3.14 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/media-utils/index.js 5.3 kB 0 B
build/notices/index.js 1.85 kB 0 B
build/nux/index.js 3.4 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.54 kB 0 B
build/primitives/index.js 1.42 kB 0 B
build/priority-queue/index.js 791 B 0 B
build/redux-routine/index.js 2.84 kB 0 B
build/reusable-blocks/index.js 2.92 kB 0 B
build/rich-text/index.js 13.3 kB 0 B
build/server-side-render/index.js 2.76 kB 0 B
build/token-list/index.js 1.27 kB 0 B
build/url/index.js 3.02 kB 0 B
build/viewport/index.js 1.86 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.22 kB 0 B

compressed-size-action

@adamziel
Copy link
Contributor Author

adamziel commented Dec 1, 2020

It seems like a number of components relies on some actions (like undo()) being synchronous so I did that by relying on defaultRegistry. Let's see what happens to e2e tests now.

@adamziel
Copy link
Contributor Author

adamziel commented Dec 1, 2020

Tests still fail, it might be more complex then. I will proceed here by changing one action and resolver at a time and observing the effects.

@jsnajdr
Copy link
Member

jsnajdr commented Jan 18, 2021

I updated this PR to use a thunk-like API that's very similar to its counterpart in Redux. An action creator returns a function that is called by the store when the action is being dispatched, and receives select, resolveSelect, dispatch, and registry arguments that let the action do things with the store and with the entire registry.

const myAction = ( param ) => async ( { select, resolveSelect, dispatch, registry } ) => {
  // call this store's selector, synchronously
  const widgets = select.getWidgets();

  // call a custom selector on this store, no need to make it part of public API
  const posts = select( state => state.posts );

  // dispatch an action
  dispatch.createWidget( toWidget( param ) );

  // some actions return a promise, let's await it
  await dispatch.saveEditedWidgets();

  // wait for selector resolution
  const posts = await resolveSelect.getPosts();

  // work with posts that are guaranteed to be fetched by now
  workWith( posts );
}

// dispatch the action like any other
dispatch( 'mystore' ).myAction( theParam );

This approach doesn't require any rungen, redux-routine and controls framework. It's just a very simple Redux store middleware, and then everything else are plain JavaScript functions calling other functions.

I adapted @adamziel's complete refactor of the core-data actions and resolvers. E2E tests seem to be fine, except the edit-widgets ones, where we conflict with the tremendous hacks that the batch requests are doing 😢 I'm afraid the batching will need to be rewritten rather completely.

@noisysocks
Copy link
Member

I'm afraid the batching will need to be rewritten rather completely.

I have good news! #28210

@noisysocks
Copy link
Member

Love this @jsnajdr!

This makes it easier to compose asynchronous and synchronous bits of code. Right now there's no real nice way to call multiple asynchronous actions at once. You have to use the global dispatch function and __unstableAwaitPromise which is what I did to implement __experimentalPerformBatch in #28210. With this it's trivial:

await Promise.all( [
	dispatch.saveEntityRecord( ... ),
	dispatch.saveEntityRecord( ... ),
] );

I think using native async / await syntax is a big selling point, too. I haven't tested it, but this probably makes it easier to debug actions using DevTools. I don't know if it's just a bug in Firefox but I can never seem to step over a yield statement.

How does this affect unit tests? The one advantage of using controls is that they're easy to test because it's just plain JavaScript objects that are yielded.

@noisysocks
Copy link
Member

The dispatch( fn ) API is a big help for #28210 too because it means we can dispatch "private" actions and therefore not need to pass around an __experimentalBatch argument.

@jsnajdr
Copy link
Member

jsnajdr commented Jan 19, 2021

Right now there's no real nice way to call multiple asynchronous actions at once.

The rungen library has a support for that: the fork and join controls. They are not, however, exposed by the redux-routine wrapper. That wrapper would have to add them to each rungen runtime it creates:

import { create, asyncControls } from 'rungen';

const rungenRuntime = create( [ ...userControls, ...asyncControls ] );

Then you could run a batch like this:

import { fork, join } from 'rungen';

function* runBatch() {
  const batch = yield createBatch();
  const duneTask = yield fork( batch.add, { path: '/v1/books', title: ... } );
  const lotrTask = yield fork( batch.add, { path: '/v1/books', title: ... } );
  yield batch.run();
  const duneResult = yield join( duneTask );
  const lotrResult = yield join( lotrTask );
}

createBatch, batch.add and batch.run would have to be registered controls, not simple functions and methods.

One downside is that join fails when called on an already finished task -- that looks like an unfortunate bug (fyi @youknowriad). yield batch.run() will wait, and so does yield join, so the task will likely be already finished by the time of yield join. With promises, we can add a .then handler at any time, even if the promise is already resolved, and it's perfectly OK.

The rungen async controls also include a promise one that does the same thing as AWAIT_PROMISE. You could simply do:

const value = yield Promise.resolve( 1 );
expect( value ).toBe( 1 );

just with the built-in controls, without registering new ones.

@jsnajdr
Copy link
Member

jsnajdr commented Jan 19, 2021

The dispatch( fn ) API is a big help for #28210 too because it means we can dispatch "private" actions

At this moment I'm exposing dispatch( fn ) and select( fn ) as public API that can be used, e.g., by the useDispatch hook:

const coreDispatch = useDispatch( 'core/data' );
coreDispatch( { type: 'FOO' } );

const coreFoo = useSelect( select => select( 'core/data' )( state => state.foo ) );

I'm not sure if that's a good idea, especially with select where it opens access to the store's internal state that until now has been encapsulated behind "official" registered selectors.

I'll change the implementation to expose the () call form only to the thunk API -- it's used to implement the store internals and deserves a more private access.

@jsnajdr
Copy link
Member

jsnajdr commented Jan 19, 2021

How does this affect unit tests? The one advantage of using controls is that they're easy to test because it's just plain JavaScript objects that are yielded.

That's true, if we want to test the thunks the same way we're testing the generators now, we'll need to mock most functions that are called by them. And then verify that these mocks got called with the expected arguments and in the expected order.

I personally don't like this style of unit testing very much. It's easy to do, but it's more like cross-checking the internal implementation rather than testing its behavior.

@jsnajdr
Copy link
Member

jsnajdr commented Jan 19, 2021

I divided the patch into two parts:

  • add the thunk middleware to all data stores
  • refactor the core-data actions and resolvers to verify that the concept works

The first part, adding the thunk middleware, is close to being mergeable. We only need to figure out how to flag the feature as experimental and make it opt-in for stores. Maybe with a flag like this?

store = createReduxStore( 'core', {
  reducer,
  controls,
  ...,
  __experimentalUseThunks: true
} );

Without the __experimentalUseThunks flag, the store creator wouldn't accept thunks, i.e., actions that are neither plain objects with type nor generators, as valid actions.

The second part, migrating core-data, needs update of unit tests and also the currently broken batching needs to be fixed.

@adamziel
Copy link
Contributor Author

This is such a great progress @jsnajdr, thank you for pushing it forward!

We only need to figure out how to flag the feature as experimental and make it opt-in for stores. Maybe with a flag like this?

The __experimentalUseThunks: true flag you proposed looks pretty good and feels well-placed – the store factory controls which features are enabled and which are not.

@adamziel
Copy link
Contributor Author

That's true, if we want to test the thunks the same way we're testing the generators now, we'll need to mock most functions that are called by them. And then verify that these mocks got called with the expected arguments and in the expected order.

This sounds like a good use case for react-testing-library. We could have a minimal consumer for each action and selector, e.g. a component that's just a button saving an entity, a span displaying information fetched via useSelect etc. This way we would be testing features, not internal implementation details, and yet it would be much faster than e2e. The only problem would be with more advanced tests such as a specific race conditions – we may need mocks for these after all.

@adamziel
Copy link
Contributor Author

adamziel commented Jan 19, 2021

The rungen async controls also include a promise one that does the same thing as AWAIT_PROMISE. You could simply do:
const value = yield Promise.resolve( 1 );
expect( value ).toBe( 1 );

My memory on that is hazy, but I think there was a corner case when a promise resolved with something resembling an action and rungen tried to dispatch it. I'm not sure though, it would be good to double check.

@jsnajdr
Copy link
Member

jsnajdr commented Jan 20, 2021

My memory on that is hazy, but I think there was a corner case when a promise resolved with something resembling an action and rungen tried to dispatch it.

Oh yes, that's indeed what will happen. The rungen's native promise control will process the resolved value again, just as if it was synchronously passed to yield.

@jsnajdr jsnajdr changed the title [Try] Async actions to replace rungen and controls Add thunk actions to replace rungen and controls Jan 21, 2021
@jsnajdr jsnajdr self-assigned this Jan 21, 2021
@jsnajdr jsnajdr added the [Package] Data /packages/data label Jan 21, 2021
@jsnajdr
Copy link
Member

jsnajdr commented Jan 21, 2021

This PR is now ready for review and merging. It adds a new experimental flag to data store config:

store = createReduxStore( 'core', {
  reducer,
  controls,
  ...,
  __experimentalUseThunks: true
} );

When enabled, actions can be implemented as thunk functions.

The core-data refactoring has been moved to a separate PR (#28389) stacked on top of this one. There's still a lot of work to be done before it really works 100%.

@adamziel
Copy link
Contributor Author

I can't approve my own PR so cc @youknowriad @noisysocks :-)

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.

@adamziel I can approve the PR on your behalf if that helps 😆

@@ -102,47 +101,6 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
return result;
}

const getResolveSelectors = memize(
Copy link
Member

Choose a reason for hiding this comment

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

Does removing this usage of memize impact performance?

Copy link
Member

Choose a reason for hiding this comment

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

I ran npm run test-performance with and without this branch and didn't see any regression.

Copy link
Member

Choose a reason for hiding this comment

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

Good point. Removing memize impacts performance, but I don't think it's measurable in practice.

Without memize, the selectors are mapped (i.e., the object with resolution functions is created) from scratch every time registry.resolveSelect() is called. This is not needed, the mapping can be done only once.

I'm fixing this in #28544 by doing the mapping only once at store initialization.

Copy link
Member

@noisysocks noisysocks left a comment

Choose a reason for hiding this comment

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

Code makes sense to me. Tests pass. It's all behind an __experimental flag. I say let's try it! 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Data /packages/data [Type] Experimental Experimental feature or API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants