-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Data: Add redux-routine package for synchronous generator flow #8096
Changes from all commits
2d3412f
46e696f
4fe2e50
568b482
31b9f42
5d9b7ad
a675f49
c1f1c35
421c3a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 ) { | ||
|
@@ -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 ) { | ||
|
@@ -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 ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: What do you think about the inconsistency of the actions behavior?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you suggesting we should promote I'm not really sure there's an inconsistency here, in that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm just thinking that maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I see it, the semantic purpose for an action is to express an intent. To that end, we traditionally consider its use in the context of dispatching within the store, but I don't see it as being fundamentally different from what we're proposing with controls, where the intent is processed as defined by the continuation procedure via the middleware. It kinda speaks back to my thoughts at #8096 (comment), where there are multiple things going on here: namely the handling of generator and the potentially-asynchronous continuation. I'm led to think that they are complementary for the purposes we're using them for, but also that it leads to open questions on:
Both seem like implementation details that the action creator needn't be concerned with. It could be asynchronous, or it could not. From the developer's perspective, it's important that it's consistent in how it's used: yielding can assign a value, whether that's assigned asynchronously or not. It's a nice bonus that it provides a solution for a common use-case (multi-dispatch). Thinking on how this is at all different from effects, the one thing that stood out to me is that we quickly turned to effects as they were the only option to do either asynchronous or multi-dispatch for a while. And once something was converted to an effect, it became that much more convenient to stay in the effect handler to perform the myriad of behaviors associated with an action. By contrast, with the routines / controls, it establishes a simple and obvious pattern to temporarily escape out of the flow from within the action creator itself in an isolated fashion. |
||
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 | ||
|
@@ -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` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should probably add mention of this requiring the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in a675f49 |
||
|
||
_**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. | ||
|
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; | ||
}, | ||
}; | ||
} |
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'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package-lock=false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: I passed this argument for consistency with how we register other scripts, but it's not obvious to me why we need
$in_footer
assigned for these registered scripts.