Often with any sort of state in a given application, there can be times in an application’s lifecycle where you want to be able to execute some sort of logic as a part of the flow in setting state or updating state. A good example of this is asynchronous data flows between the client and server sides of your application (eg. persisting state to a server database via a REST API request).
In wp.data, a control (or control function) defines the execution flow behavior associated with a specific action type. You can then use the control action creators in store action creators, and resolvers, and you can register defined controls to handle this execution as instructed within your store.
Let’s take a look at an example control action and corresponding control function found in the src/data/controls.js
file within the example app:
export const fetch = (path, options = {}) => {
if (options.body) {
options.body = JSON.stringify(options.body);
options.headers = { "Content-Type": "application/json" };
}
return {
type: "FETCH",
path,
options
};
};
export default {
FETCH({ path, options }) {
return new Promise((resolve, reject) => {
window
.fetch(path, options)
.then(response => response.json())
.then(result => resolve(result))
.catch(error => reject(error));
});
}
};
At the top of the file is the fetch
control action creator which receives the same argument shape as the window.fetch
function and then returns a control action object that has FETCH
as the action type, and passes along the path
and options
property and values.
Then we are exporting a default object that is our control object that has all the defined control functions. In this object is a FETCH
property that has a function as a callback, this function is the control function that will get invoked when the control action with the FETCH
type is returned or yielded from an action creator or a resolver.
This default object is what would get registered to your store as the value for the controls
object. Let’s go over a few rules of this object:
- Each property is the same value as the control action type you want the defined control function to react to.
- The value attached to each property is a control function which receives the original action object associated with the control action type.
- The control function should return ether a
Promise
or any other value.
In this series, we will have opportunity to implement controls in the example application, however to help with visualizing how controls are implemented, here’s a quick look at an example store action creator that has been updated to be a store action creator generator implementing controls:
import { fetch } from './controls';
export function* updateAndPersistThing = ( thing ) => {
// catch any request errors.
try {
// execution will pause here until the `FETCH` control function's return
// value has resolved.
const updatedThing = yield fetch(
'https://thingupdaterapi.com/things',
{ method: 'PUT', data: thing }
);
} catch( error ) {
// returning an action object that will save the update error to the state.
return { type: 'THING_UPDATE_ERROR', message: error.message };
}
if ( updatedThing ) {
// thing was successfully updated so return the action object that will
// update the saved thing in the state.
return { type: 'UPDATE_THING', thing }
}
// if execution arrives here, then thing didn't update in the state so return
// action object that will add an error to the state about this.
return { type: 'THING_UPDATE_ERROR', message: 'Thing did not get updated.' }
}
You implement control action creators in either action creators or resolvers that are defined as generators which yield action types. When wp.data encounters an action creator (or resolver) as a generator as a part of the execution flow, under the hood it steps through each yielded value and for controls that return a promise, it will await the resolution of the promise and return that as the value of the yield assignment. If the control handler returns undefined, the execution is not continued.
Sidenote: Generators are a whole subject on their own that is good to have an understanding of to fully comprehend how wp.data is using them. A good resource for understanding generators is this MDN article or this post.
Controls can be one of the harder interfaces of wp.data to understand, yet once you do, there is immediate benefit in having a clearer flow of execution for actions and resolvers having side-effects.
The good news, is there is a package available that exposes three commonly used controls for usage in your stores,@wordpress/data-controls
. The controls exposed in this package will cover most of the use-cases you might have for controls in a custom data store, in a WordPress environment. The controls exposed by this package are:
This control action creator will yield an action type that triggers a control function for performing an api fetch call using the object provided to the action creator. Essentially it uses the @wordpress/api-fetch
interface, but wrapped in a control for usage in wp.data action creator and resolver generators.
It should be noted that this implementation is currently coupled to interacting with the WordPress REST api (PUT
, and DELETE
methods are sent on the request as a header instead of as the request type and the service we’re using doesn’t know about the header). Thus for the example application used in this series, I’ve created a custom control that implements window.fetch
directly. This is the example referenced at the beginning of this article.
This control action creator will yield an action type that can be used to dispatch another action in a different store within the same data registry.
This control action creator will yield an action type that can be used to select data from the state in another store within the same data registry.
Note: this control will automatically detect if the selector is attached to a resolver and will wait for the resolution to finish before returning the value of the selector. We haven’t addressed resolvers yet in this series, but don’t worry, we’re going to jump into that in the next post!
The best way to understand the usage of controls is to, well, use them. So in the next post we’ll jump back to learning about the resolvers
property in the registerStore
configuration object.