From 79d9ca5c7f9f76adc44810cf76672de4a10b8dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 28 Oct 2021 17:30:39 +0200 Subject: [PATCH 1/6] Add Thunks tutorial --- docs/how-to-guides/thunks.md | 228 +++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 docs/how-to-guides/thunks.md diff --git a/docs/how-to-guides/thunks.md b/docs/how-to-guides/thunks.md new file mode 100644 index 00000000000000..aba4baece7a488 --- /dev/null +++ b/docs/how-to-guides/thunks.md @@ -0,0 +1,228 @@ +# Thunks in Core-Data + +[Gutenberg 11.6](https://github.com/WordPress/gutenberg/pull/27276) added supports for thunks. You can think of thunks as of functions that can be dispatched: + +```js +// actions.js +export const myThunkAction = () => ( { select, dispatch } ) => { + return "I'm a thunk! I can be dispatched, use selectors, and even dispatch other actions."; +}; +``` + +## Why Are Thunks Useful? + +Thunks [expand the meaning of what a Redux action is](https://jsnajdr.wordpress.com/2021/10/04/motivation-for-thunks/). Before thunks, actions were purely functional and could only return and yield data. Common use-cases such as interacting with the store or requesting API data from an action required using a separate [control](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/#controls). You would often see code like: + +```js +export function* saveRecordAction( id ) { + const record = yield controls.select( 'current-store', 'getRecord', id ); + yield { type: 'BEFORE_SAVE', id, record }; + const results = yield controls.fetch({ url: 'https://...', method: 'POST', data: record }); + yield { type: 'AFTER_SAVE', id, results }; + return results; +} + +const controls = { + select: // ..., + fetch: // ..., +}; +``` + +Side-effects like store operations and fetch functions would be implemented outside of the action. Thunks provide alternative to this approach. They allow you to use side-effects inline, like this: + +```js +export const saveRecordAction = ( id ) => async ({ select, dispatch }) => { + const record = select( 'current-store', 'getRecord', id ); + dispatch({ type: 'BEFORE_SAVE', id, record }); + const response = await fetch({ url: 'https://...', method: 'POST', data: record }); + const results = await response.json(); + dispatch({ type: 'AFTER_SAVE', id, results }); + return results; +} +``` + +This removes the need to implement separate controls. + +### Thunks Have Access to the Store Helpers + +Let's take a look at an example from Gutenberg core. Prior to thunks, the `toggleFeature` action from the `@wordpress/interface` package was implemented like this: + +```js +export function* toggleFeature( scope, featureName ) { + const currentValue = yield controls.select( + interfaceStoreName, + 'isFeatureActive', + scope, + featureName + ); + + yield controls.dispatch( + interfaceStoreName, + 'setFeatureValue', + scope, + featureName, + ! currentValue + ); +} +``` + +Controls were the only way to `dispatch` actions and `select` data from the store. + +With thunks, there is a cleaner way. This is how `toggleFeature` is implemented now: + +```js +export function toggleFeature( scope, featureName ) { + return function ( { select, dispatch } ) { + const currentValue = select.isFeatureActive( scope, featureName ); + dispatch.setFeatureValue( scope, featureName, ! currentValue ); + }; +} +``` + +Thanks to the `select` and `dispatch` arguments, thunks may use the store directly without the need for generators and controls. + +### Thunks may be async + +Imagine a simple React app that allows you to set the temperature on a thermostat. It only has one input and one button. Clicking the button dispatches a `saveTemperatureToAPI` action with the value from the input. + +If we used controls to save the temperature, the store definition would look like below: + +```js +const store = wp.data.createReduxStore( 'my-store', { + actions: { + saveTemperatureToAPI: function*( temperature ) { + const result = yield { type: 'FETCH_JSON', url: 'https://...', method: 'POST', data: { temperature } }; + return result; + } + }, + controls: { + async FETCH_JSON( action ) { + const response = await window.fetch( action.url, { + method: action.method, + body: JSON.stringify( action.data ), + } ); + return response.json(); + } + }, + // reducers, selectors, ... +} ); +``` + +While the code is reasonably straightforward, there is a level of indirection. The `saveTemperatureToAPI` action does not talk directly to the API, but has to go through the `FETCH_JSON` control. + +Let's see how this indirection can be removed with thunks: + +```js +const store = wp.data.createReduxStore( 'my-store', { + __experimentalUseThunks: true, + actions: { + saveTemperatureToAPI: ( temperature ) => async () => { + const response = await window.fetch( 'https://...', { + method: 'POST', + body: JSON.stringify( { temperature } ), + } ); + return await response.json(); + } + }, + // reducers, selectors, ... +} ); +``` + +That's pretty cool! What's even better is that resolvers are supported as well: + +```js +const store = wp.data.createReduxStore( 'my-store', { + // ... + selectors: { + getTemperature: ( state ) => state.temperature + }, + resolvers: { + getTemperature: () => async ( { dispatch } ) => { + const response = await window.fetch( 'https://...' ); + const result = await response.json(); + dispatch.receiveCurrentTemperature( result.temperature ); + } + }, + // ... +} ); +``` + +Thunks' support is experimental for now. You can enable it by setting `__experimentalUseThunks: true` when registering your store. + +## Thunks API + +A thunk receives a single object argument with the following keys: + +### select + +An object containing the store’s selectors pre-bound to state, which means you don't need to provide the state, only the additional arguments. `select` triggers the related resolvers, if any, but does not wait for them to finish. It just returns the current value even if it's null. + + +If a selector is part of the public API, it's available as a method on the select object: + +```js +const thunk = () => ( { select } ) => { + // select is an object of the store’s selectors, pre-bound to current state: + const temperature = select.getTemperature(); +} +``` + +Since not all selectors are exposed on the store, `select` doubles as a function that supports passing selector as an argument: + +```js +const thunk = () => ( { select } ) => { + // select supports private selectors: + const doubleTemperature = select( ( temperature ) => temperature * 2 ); +} +``` + +### resolveSelect + +`resolveSelect` is the same as `select`, except it returns a promise that resolves with the value provided by the related resolver. + +```js +const thunk = () => ( { resolveSelect } ) => { + const temperature = await resolveSelect.getTemperature(); +} +``` + +### dispatch + +An object containing the store’s actions + +If an action is part of the public API, it's available as a method on the `dispatch` object: + +```js +const thunk = () => ( { dispatch } ) => { + // dispatch is an object of the store’s actions: + const temperature = await dispatch.retrieveTemperature(); +} +``` + +Since not all actions are exposed on the store, `dispatch` doubles as a function that supports passing a redux action as an argument: + +```js +const thunk = () => async ( { dispatch } ) => { + // dispatch is also a function accepting inline actions: + dispatch({ type: 'SET_TEMPERATURE', temperature: result.value }); + + // thunks are interchangeable with actions + dispatch( updateTemperature( 100 ) ); + + // Thunks may be async, too. When they are, dispatch returns a promise + await dispatch( ( ) => window.fetch( /* ... */ ) ); +} +``` + +### registry + +Registry provides access to other stores through `dispatch`, `select`, and `resolveSelect` methods. +They are very similar to the ones described above, with a slight twist. Calling `registry.select( storeName )` returns a function returning an object of selectors from `storeName`. This comes handy when you need to interact with another store. For example: + +```js +const thunk = () => ( { registry } ) => { + const error = registry.select( 'core' ).getLastEntitySaveError( 'root', 'menu', menuId ); + /* ... */ +} +``` + From 2c98f3058336583e0b859a2f5fdebb8969685e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 28 Oct 2021 17:33:25 +0200 Subject: [PATCH 2/6] Add thunks.md to toc.json and manifest.json --- docs/how-to-guides/README.md | 2 +- docs/manifest.json | 6 ++++++ docs/toc.json | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/how-to-guides/README.md b/docs/how-to-guides/README.md index 198108e5783167..918e20edfe6d63 100644 --- a/docs/how-to-guides/README.md +++ b/docs/how-to-guides/README.md @@ -27,7 +27,7 @@ You can also filter certain aspects of the editor; this is documented on the [Ed Porting PHP meta boxes to blocks or sidebar plugins is highly encouraged, learn how through these [meta data tutorials](/docs/how-to-guides/metabox/README.md). See how the new editor [supports existing Meta Boxes](/docs/reference-guides/backward-compatibility/meta-box.md). - +ń ## Theme Support By default, blocks provide their styles to enable basic support for blocks in themes without any change. Themes can add/override these styles, or rely on defaults. diff --git a/docs/manifest.json b/docs/manifest.json index dfab359df6cf29..dd2955cadb0c08 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -407,6 +407,12 @@ "markdown_source": "../docs/how-to-guides/internationalization.md", "parent": "how-to-guides" }, + { + "title": "Thunks", + "slug": "thunks", + "markdown_source": "../docs/how-to-guides/thunks.md", + "parent": "how-to-guides" + }, { "title": "Widgets", "slug": "widgets", diff --git a/docs/toc.json b/docs/toc.json index b21d13e7a2071e..b65cfd3cde5688 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -171,6 +171,7 @@ }, { "docs/how-to-guides/accessibility.md": [] }, { "docs/how-to-guides/internationalization.md": [] }, + { "docs/how-to-guides/thunks.md": [] }, { "docs/how-to-guides/widgets/README.md": [ { "docs/how-to-guides/widgets/overview.md": [] }, From a4bc0aac75a3461e9117a05a848cb2a9c204dde6 Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Thu, 28 Oct 2021 17:37:15 +0200 Subject: [PATCH 3/6] Remove dev artifact --- docs/how-to-guides/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to-guides/README.md b/docs/how-to-guides/README.md index 918e20edfe6d63..198108e5783167 100644 --- a/docs/how-to-guides/README.md +++ b/docs/how-to-guides/README.md @@ -27,7 +27,7 @@ You can also filter certain aspects of the editor; this is documented on the [Ed Porting PHP meta boxes to blocks or sidebar plugins is highly encouraged, learn how through these [meta data tutorials](/docs/how-to-guides/metabox/README.md). See how the new editor [supports existing Meta Boxes](/docs/reference-guides/backward-compatibility/meta-box.md). -ń + ## Theme Support By default, blocks provide their styles to enable basic support for blocks in themes without any change. Themes can add/override these styles, or rely on defaults. From 8b14dbf6fcf3afe84125bf52fba26cf63ce72fad Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Thu, 28 Oct 2021 17:59:45 +0200 Subject: [PATCH 4/6] Update manifest.json --- docs/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index dd2955cadb0c08..036e86386d6d17 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -408,8 +408,8 @@ "parent": "how-to-guides" }, { - "title": "Thunks", - "slug": "thunks", + "title": "Thunks in Core-Data", + "slug": "thunks-in-core-data", "markdown_source": "../docs/how-to-guides/thunks.md", "parent": "how-to-guides" }, From 802353b7e3ac2909ea76c351e95f8e24ed4669a2 Mon Sep 17 00:00:00 2001 From: Miguel Fonseca Date: Thu, 28 Oct 2021 17:29:24 +0100 Subject: [PATCH 5/6] (minor) Tweak copy --- docs/how-to-guides/thunks.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/how-to-guides/thunks.md b/docs/how-to-guides/thunks.md index aba4baece7a488..13538ddd8873c9 100644 --- a/docs/how-to-guides/thunks.md +++ b/docs/how-to-guides/thunks.md @@ -1,6 +1,6 @@ # Thunks in Core-Data -[Gutenberg 11.6](https://github.com/WordPress/gutenberg/pull/27276) added supports for thunks. You can think of thunks as of functions that can be dispatched: +[Gutenberg 11.6](https://github.com/WordPress/gutenberg/pull/27276) added support for _thunks_. You can think of thunks as functions that can be dispatched: ```js // actions.js @@ -9,9 +9,9 @@ export const myThunkAction = () => ( { select, dispatch } ) => { }; ``` -## Why Are Thunks Useful? +## Why are thunks useful? -Thunks [expand the meaning of what a Redux action is](https://jsnajdr.wordpress.com/2021/10/04/motivation-for-thunks/). Before thunks, actions were purely functional and could only return and yield data. Common use-cases such as interacting with the store or requesting API data from an action required using a separate [control](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/#controls). You would often see code like: +Thunks [expand the meaning of what a Redux action is](https://jsnajdr.wordpress.com/2021/10/04/motivation-for-thunks/). Before thunks, actions were purely functional and could only return and yield data. Common use cases such as interacting with the store or requesting API data from an action required using a separate [control](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/#controls). You would often see code like: ```js export function* saveRecordAction( id ) { @@ -28,7 +28,7 @@ const controls = { }; ``` -Side-effects like store operations and fetch functions would be implemented outside of the action. Thunks provide alternative to this approach. They allow you to use side-effects inline, like this: +Side effects like store operations and fetch functions would be implemented outside of the action. Thunks provide an alternative to this approach. They allow you to use side effects inline, like this: ```js export const saveRecordAction = ( id ) => async ({ select, dispatch }) => { @@ -43,7 +43,7 @@ export const saveRecordAction = ( id ) => async ({ select, dispatch }) => { This removes the need to implement separate controls. -### Thunks Have Access to the Store Helpers +### Thunks have access to the store helpers Let's take a look at an example from Gutenberg core. Prior to thunks, the `toggleFeature` action from the `@wordpress/interface` package was implemented like this: @@ -81,7 +81,7 @@ export function toggleFeature( scope, featureName ) { Thanks to the `select` and `dispatch` arguments, thunks may use the store directly without the need for generators and controls. -### Thunks may be async +### Thunks can be async Imagine a simple React app that allows you to set the temperature on a thermostat. It only has one input and one button. Clicking the button dispatches a `saveTemperatureToAPI` action with the value from the input. @@ -147,7 +147,7 @@ const store = wp.data.createReduxStore( 'my-store', { } ); ``` -Thunks' support is experimental for now. You can enable it by setting `__experimentalUseThunks: true` when registering your store. +Support for thunks is experimental for now. You can enable it by setting `__experimentalUseThunks: true` when registering your store. ## Thunks API @@ -167,7 +167,7 @@ const thunk = () => ( { select } ) => { } ``` -Since not all selectors are exposed on the store, `select` doubles as a function that supports passing selector as an argument: +Since not all selectors are exposed on the store, `select` doubles as a function that supports passing a selector as an argument: ```js const thunk = () => ( { select } ) => { @@ -199,7 +199,7 @@ const thunk = () => ( { dispatch } ) => { } ``` -Since not all actions are exposed on the store, `dispatch` doubles as a function that supports passing a redux action as an argument: +Since not all actions are exposed on the store, `dispatch` doubles as a function that supports passing a Redux action as an argument: ```js const thunk = () => async ( { dispatch } ) => { @@ -216,8 +216,8 @@ const thunk = () => async ( { dispatch } ) => { ### registry -Registry provides access to other stores through `dispatch`, `select`, and `resolveSelect` methods. -They are very similar to the ones described above, with a slight twist. Calling `registry.select( storeName )` returns a function returning an object of selectors from `storeName`. This comes handy when you need to interact with another store. For example: +A registry provides access to other stores through its `dispatch`, `select`, and `resolveSelect` methods. +These are very similar to the ones described above, with a slight twist. Calling `registry.select( storeName )` returns a function returning an object of selectors from `storeName`. This comes handy when you need to interact with another store. For example: ```js const thunk = () => ( { registry } ) => { From 91635c1dfafc49fbe326ba5997c057a29efb43f2 Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Fri, 29 Oct 2021 11:20:24 +0200 Subject: [PATCH 6/6] Update manifest.json --- docs/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index 036e86386d6d17..2e59ad346ef6a6 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -409,7 +409,7 @@ }, { "title": "Thunks in Core-Data", - "slug": "thunks-in-core-data", + "slug": "thunks", "markdown_source": "../docs/how-to-guides/thunks.md", "parent": "how-to-guides" },