Skip to content

Commit

Permalink
Data: Add redux-routine package for synchronous generator flow (#8096)
Browse files Browse the repository at this point in the history
* Packages: Add package `redux-routine`

* Packages: Throw rejected promise as error to generator

* Data: Add support for controls via redux-routine

* Core Data: Partially reimplement resolvers as controls

* Data: Port controls as data plugin

* Data: Revert deprecation of async generator

* Docs: Add note about plugin opt-in for controls

* ReduxRoutine: Improve isGenerator accuracy

* Fix redux-routine unit tests
  • Loading branch information
aduth authored and youknowriad committed Aug 7, 2018
1 parent 38378c8 commit 1949e3a
Show file tree
Hide file tree
Showing 18 changed files with 592 additions and 37 deletions.
20 changes: 18 additions & 2 deletions lib/client-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,14 @@ function gutenberg_register_scripts_and_styles() {
wp_register_script(
'wp-data',
gutenberg_url( 'build/data/index.js' ),
array( 'wp-deprecated', 'wp-element', 'wp-compose', 'wp-is-shallow-equal', 'lodash' ),
array(
'wp-deprecated',
'wp-element',
'wp-compose',
'wp-is-shallow-equal',
'lodash',
'wp-redux-routine',
),
filemtime( gutenberg_dir_path() . 'build/data/index.js' ),
true
);
Expand All @@ -222,7 +229,9 @@ function gutenberg_register_scripts_and_styles() {
' localStorage[ storageKey ] = localStorage[ oldStorageKey ];',
' delete localStorage[ oldStorageKey ];',
' }',
' wp.data.use( wp.data.plugins.persistence, { storageKey: storageKey } );',
' wp.data',
' .use( wp.data.plugins.persistence, { storageKey: storageKey } )',
' .use( wp.data.plugins.controls );',
'} )()',
) )
);
Expand Down Expand Up @@ -268,6 +277,13 @@ function gutenberg_register_scripts_and_styles() {
filemtime( gutenberg_dir_path() . 'build/shortcode/index.js' ),
true
);
wp_register_script(
'wp-redux-routine',
gutenberg_url( 'build/redux-routine/index.js' ),
array(),
filemtime( gutenberg_dir_path() . 'build/redux-routine/index.js' ),
true
);
wp_add_inline_script( 'wp-utils', 'var originalUtils = window.wp && window.wp.utils ? window.wp.utils : {};', 'before' );
wp_add_inline_script( 'wp-utils', 'for ( var key in originalUtils ) wp.utils[ key ] = originalUtils[ key ];' );
wp_register_script(
Expand Down
31 changes: 30 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@wordpress/keycodes": "file:packages/keycodes",
"@wordpress/nux": "file:packages/nux",
"@wordpress/plugins": "file:packages/plugins",
"@wordpress/redux-routine": "file:packages/redux-routine",
"@wordpress/shortcode": "file:packages/shortcode",
"@wordpress/url": "file:packages/url",
"@wordpress/viewport": "file:packages/viewport",
Expand Down
93 changes: 67 additions & 26 deletions packages/data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,38 @@ npm install @wordpress/data --save
Use the `registerStore` function to add your own store to the centralized data registry. This function accepts two arguments: a name to identify the module, and an object with values describing how your state is represented, modified, and accessed. At a minimum, you must provide a reducer function describing the shape of your state and how it changes in response to actions dispatched to the store.

```js
const { data, fetch } = wp;
const { registerStore, dispatch } = data;
const { data, apiFetch } = wp;
const { registerStore } = data;

const DEFAULT_STATE = {
prices: {},
discountPercent: 0,
};

const actions = {
setPrice( item, price ) {
return {
type: 'SET_PRICE',
item,
price,
};
},

startSale( discountPercent ) {
return {
type: 'START_SALE',
discountPercent,
};
},

fetchFromAPI( path ) {
return {
type: 'FETCH_FROM_API',
path,
};
},
};

registerStore( 'my-shop', {
reducer( state = DEFAULT_STATE, action ) {
switch ( action.type ) {
Expand All @@ -47,21 +71,7 @@ registerStore( 'my-shop', {
return state;
},

actions: {
setPrice( item, price ) {
return {
type: 'SET_PRICE',
item,
price,
};
},
startSale( discountPercent ) {
return {
type: 'START_SALE',
discountPercent,
};
},
},
actions,

selectors: {
getPrice( state, item ) {
Expand All @@ -72,21 +82,22 @@ registerStore( 'my-shop', {
},
},

controls: {
FETCH_FROM_API( action ) {
return apiFetch( { path: action.path } );
},
},

resolvers: {
async getPrice( state, item ) {
const price = await apiFetch( { path: '/wp/v2/prices/' + item } );
dispatch( 'my-shop' ).setPrice( item, price );
* getPrice( state, item ) {
const path = '/wp/v2/prices/' + item;
const price = yield actions.fetchFromAPI( path );
return actions.setPrice( item, price );
},
},
} );
```

A [**reducer**](https://redux.js.org/docs/basics/Reducers.html) is a function accepting the previous `state` and `action` as arguments and returns an updated `state` value.

The **`actions`** object should describe all [action creators](https://redux.js.org/glossary#action-creator) available for your store. An action creator is a function that optionally accepts arguments and returns an action object to dispatch to the registered reducer. _Dispatching actions is the primary mechanism for making changes to your state._

The **`selectors`** object includes a set of functions for accessing and deriving state values. A selector is a function which accepts state and optional arguments and returns some value from state. _Calling selectors is the primary mechanism for retrieving data from your state_, and serve as a useful abstraction over the raw data which is typically more susceptible to change and less readily usable as a [normalized object](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape#designing-a-normalized-state).

The return value of `registerStore` is a [Redux-like store object](https://redux.js.org/docs/basics/Store.html) with the following methods:

- `store.getState()`: Returns the state value of the registered reducer
Expand All @@ -96,6 +107,36 @@ The return value of `registerStore` is a [Redux-like store object](https://redux
- `store.dispatch( action: Object )`: Given an action object, calls the registered reducer and updates the state value.
- _Redux parallel:_ [`dispatch`](https://redux.js.org/api-reference/store#dispatch(action))

## Options

### `reducer`

A [**reducer**](https://redux.js.org/docs/basics/Reducers.html) is a function accepting the previous `state` and `action` as arguments and returns an updated `state` value.

### `actions`

The **`actions`** object should describe all [action creators](https://redux.js.org/glossary#action-creator) available for your store. An action creator is a function that optionally accepts arguments and returns an action object to dispatch to the registered reducer. _Dispatching actions is the primary mechanism for making changes to your state._

### `selectors`

The **`selectors`** object includes a set of functions for accessing and deriving state values. A selector is a function which accepts state and optional arguments and returns some value from state. _Calling selectors is the primary mechanism for retrieving data from your state_, and serve as a useful abstraction over the raw data which is typically more susceptible to change and less readily usable as a [normalized object](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape#designing-a-normalized-state).

### `resolvers`

A **resolver** is a side-effect for a selector. If your selector result may need to be fulfilled from an external source, you can define a resolver such that the first time the selector is called, the fulfillment behavior is effected.

The `resolvers` option should be passed as an object where each key is the name of the selector to act upon, the value a function which receives the same arguments passed to the selector. It can then dispatch as necessary to fulfill the requirements of the selector, taking advantage of the fact that most data consumers will subscribe to subsequent state changes (by `subscribe` or `withSelect`).

### `controls`

_**Note:** Controls are an opt-in feature, enabled via `use` (the [Plugins API](https://github.com/WordPress/gutenberg/tree/master/packages/data/src/plugins))._

A **control** defines the execution flow behavior associated with a specific action type. This can be particularly useful in implementing asynchronous data flows for your store. By defining your action creator or resolvers as a generator which yields specific controlled action types, the execution will proceed as defined by the control handler.

The `controls` option should be passed as an object where each key is the name of the action type to act upon, the value a function which receives the original action object. It should returns either a promise which is to resolve when evaluation of the action should continue, or a value. The value or resolved promise value is assigned on the return value of the yield assignment. If the control handler returns undefined, the execution is not continued.

Refer to the [documentation of `@wordpress/redux-routine`](https://github.com/WordPress/gutenberg/tree/master/packages/redux-routine/) for more information.

## Data Access and Manipulation

It is very rare that you should access store methods directly. Instead, the following suite of functions and higher-order components is provided for the most common data access and manipulation needs.
Expand Down
3 changes: 2 additions & 1 deletion packages/data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"@wordpress/deprecated": "file:../deprecated",
"@wordpress/element": "file:../element",
"@wordpress/is-shallow-equal": "file:../is-shallow-equal",
"equivalent-key-map": "^0.2.1",
"@wordpress/redux-routine": "file:../redux-routine",
"equivalent-key-map": "^0.2.0",
"lodash": "^4.17.10",
"redux": "^3.7.2"
},
Expand Down
30 changes: 30 additions & 0 deletions packages/data/src/plugins/controls/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* External dependencies
*/
import { applyMiddleware } from 'redux';

/**
* WordPress dependencies
*/
import createMiddleware from '@wordpress/redux-routine';

export default function( registry ) {
return {
registerStore( reducerKey, options ) {
const store = registry.registerStore( reducerKey, options );

if ( options.controls ) {
const middleware = createMiddleware( options.controls );
const enhancer = applyMiddleware( middleware );
const createStore = () => store;

Object.assign(
store,
enhancer( createStore )( options.reducer )
);
}

return store;
},
};
}
1 change: 1 addition & 0 deletions packages/data/src/plugins/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as controls } from './controls';
export { default as persistence } from './persistence';
14 changes: 7 additions & 7 deletions packages/data/src/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import {
*/
import deprecated from '@wordpress/deprecated';

/**
* Internal dependencies
*/
import dataStore from './store';
import { persistence } from './plugins';

/**
* An isolated orchestrator of store registrations.
*
Expand All @@ -37,12 +43,6 @@ import deprecated from '@wordpress/deprecated';
* @typedef {WPDataPlugin}
*/

/**
* Internal dependencies
*/
import dataStore from './store';
import { persistence } from './plugins';

/**
* Returns true if the given argument appears to be a dispatchable action.
*
Expand Down Expand Up @@ -230,7 +230,7 @@ export function createRegistry( storeConfigs = {} ) {
}

for await ( const maybeAction of fulfillment ) {
// Dispatch if it quacks like an action.
// Dispatch if it quacks like an action.
if ( isActionLike( maybeAction ) ) {
store.dispatch( maybeAction );
}
Expand Down
1 change: 1 addition & 0 deletions packages/redux-routine/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
Loading

0 comments on commit 1949e3a

Please sign in to comment.