WordPress Data Store Properties: Action Creator Generators

This entry is part 9 of 15 in the series, A Practical Overview of the @wordpress/data API

Now we have a resolver for asynchronously getting products via a REST api endpoint, we have some action creators, and we have a selector.  We haven’t got to the reducer yet, but before we get to it, let’s consider another problem we haven’t addressed yet. 

When we update or create a product using our ui, how are we going to persist that to the server? We could just wire up the POST/PUT endpoints directly in our component, but remember one of the reasons we’re implementing wp.data is to provide a common interface for any component needing to interact with the products data. So what can we do here?

Fortunately, we’ve covered a part of the wp.data api that provides us with a mechanism for doing this, controls. With controls, it is possible to make action creator generators that yield control action objects for handling asynchronous behaviour.  Essentially everything you’ve learned about resolver generators so far can also be applied to action creators!

So let’s go back to the action creators we’ve already prepared (reminder: src/data/products/actions.js) which are createProduct, updateProduct and deleteProduct. Right now these are not wired up to any API route. Let’s change that. If you feel adventurous, why don’t you give it a go in your own fork of the app right now and see what you come up with. You can come back and see how close you got after.

You back? Okay, here’s one approach you could end up with:

import { getResourcePath } from './utils';
import { fetch } from '../controls';
import { select } from '@wordpress/data-controls';
import TYPES from './action-types';
import STORE_KEY from './constants';


export function* createProduct(product) {
 const result = yield fetch(getResourcePath(), {
   method: "POST",
   body: product
 if (result) {
   return {
     type: CREATE,
     product: result
export function* updateProduct(product) {
 const canonicalProduct = yield select(STORE_KEY, "getProduct", product.id);
 const result = yield fetch(getResourcePath(canonicalProduct._id), {
   method: "PUT",
   body: product
 if (result) {
   return {
     type: UPDATE,
export function* deleteProduct(productId) {
 const product = yield select(STORE_KEY, "getProduct", productId);
 const result = yield fetch(getResourcePath(product._id), {
   method: "DELETE"
 if (result) {
   return {
     type: DELETE,

Take note of a few things here:

  • For updateProduct and deleteProduct I need to get the original product in the state because jsonbox.io references objects on the server via an _id property. I could have used the existing getProducts selector and then filtered the product from the state, but I decided to create a new selector to handle that (which also keeps things DRY).  Here’s what I ended up with as a new selector in selectors.js:
export const getProduct = (state, id) => {
 return getProducts(state).find(product => product.id === id);
  • To access this new selector, I used the select control from the @wordpress/data-controls package. Why? This ensures we’re accessing the selector from the same registry (the importance of this will be explained in more detail in a later post in the series).
  • I’m using the fetch control (remember this from the post about resolvers?)
  • I’m returning the original action object that was here before the updates.

Notice that the action creator is still returning an action object, the only change here is that with controls, we’re able to do some side-effects before returning that action object.

In the next post of this series, it’s time to go on to the last property of our store registration configuration object, the reducer.

What is WordPress Data?

This entry is part 2 of 15 in the series, A Practical Overview of the @wordpress/data API

The WordPress data module (or package) is a critical component of the new WordPress Editor as demonstrated by it’s implementation in (at the time of writing) 7 other WordPress packages:

  • @wordpress/block-directory registers a store with the core/block-directory namespace.
  • @wordpress/block-editor registers a store with the core/block-editor namespace.
  • @wordpress/blocks registers a store with the core/blocks namespace.
  • @wordpress/core-data registers a store with the core namespace.
  • @wordpress/edit-post registers a store with the core/edit-post namespace.
  • @wordpress/editor registers a store with the core/editor namespace.
  • @wordpress/notices registers a store with the core/notices namespace.

What do I mean by “namespace” in the above list?
We’ll get into this in more detail later on, but as a quick answer, wp.data stores can be registered to their own state object identified by a given string which is referred to here as their namespace. So to interact with the data stored in a given state, you need to know and reference it’s namespace.

Read more “What is WordPress Data?”

WordPress Data Store Properties: Reducer

This entry is part 10 of 15 in the series, A Practical Overview of the @wordpress/data API

In the exploration through wp.data, we’ve already created the necessary properties needed to register the custom products store for our example app. This includes, action creators, selectors, controls, and resolvers.

Now it’s time to get to the last property we need for registering our store, and that’s the reducer. The reducer is arguably the most important part of the store registry and in fact it’s the only option you are required to include when registering a store.

The reducer function describes the shape of your state and how it changes in response to actions dispatched to your store. It will receive the previous state and an action object as arguments and should return an updated state value (or the same state object if nothing has changed). Here’s a very basic example reducer:

export const reducer = ( state, action ) => {
   if ( action.type === 'DO_SOMETHING' ) {
        return {

There’s a couple principles you should follow with your reducer function:

  • It must be a pure function.  Pure functions are functions that have no side effects.
  • It should never mutate the incoming state, but return a new state with any updates applied. With that said, you can split up a large state tree so that each branch is managed by it’s own reducer and then combine them all into one reducer function (using combineReducer).

With that in mind, feel free to give a go at writing a reducer for our example app and when you’re done, come back here and compare with what I’ve put together.

Done? Alright let’s compare:

import TYPES from "./action-types";
const reducer = (
 state = { products: [] },
 { products: incomingProducts, product, productId, type }
) => {
 switch (type) {
   case CREATE:
     return {
       products: [...state.products, product]
   case UPDATE:
     return {
       products: state.products
         .filter(existing => existing.id !== product.id)
   case DELETE:
     return {
       products: state.products.filter(existing => existing.id !== productId)
   case HYDRATE:
     return { products: incomingProducts };
     return state;
export default reducer;

Let’s breakdown the above example a bit. What is happening?

First, our reducer function is defining a default value for the state argument of { products: [] }. This ensures that if the state has not been initialized yet, it will assume this value. It’s a great way to describe the shape of the state in your store (especially if your store is fairly simple with a single reducer function).

Second, our reducer function is checking what type is coming from the provided action object and defining what to do if a specific type is passed in on the action object. So for instance, this particular reducer is listening for the CREATE, UPDATE, DELETE, and HYDRATE type constants and reacting accordingly.

Third, each type has logic associated with it that will update the state using the data provided by that action object. So an action object with the CREATE action type constant value will return a new state object with the passed in product added to the products array. An action object with the UPDATE action type constant value will return a new state object replacing a product in the products array with the incoming product (or adding it if it doesn’t exist in the state yet). An action object with the DELETE action type constant value will return an new state object with the products array excluding the provided product.id. An action object with the HYDRATE action type constant value will replace the products array in the state (with the provided array of products).

Finally, if the action object does not have a matching type for what the reducer is watching for, then the current state is simply returned.

The reducer plays an important role in a wp.data store because it’s the method by which the state is updated and signals to the app subscribing to the store any state changes.

In the next post of this series we can finally put all the parts together to register our store!

WordPress Data Store Properties: Resolvers

This entry is part 8 of 15 in the series, A Practical Overview of the @wordpress/data API

Before we took our side detour into learning about the controls property, I mentioned that since we now have a selector for getting products, it’d be good to implement a way to retrieve the products from the server. This is where resolvers come in.

Let’s get started by creating a resolver for retrieving products. If you’re following along, create a new file in the store folder called resolvers.js. Your file structure should end up being src/data/products/resolvers.js.  Add this to the file:

import { fetch } from "../controls";
import { hydrate } from "./actions";
import { getResourcePath } from "./utils";
export function* getProducts() {
 const products = yield fetch(getResourcePath());
 if (products) {
   return hydrate(products);

In our example, the following things are happening:

  • This first expression yields the fetch control action which returns the response from the window.fetch call when it resolves.  If you think of the yield acting like an await here, it might make more sense. The return value is assigned to products.
  • If there is a truthy value for products, then the hydrate action creator is called and the resulting value returned which essentially results in dispatching the action object for hydrating the state.

Note: getResourcePath() is just a utility I created for setting up the url interacting with the jsonbox.io api. It should be in the products/utils.js folder already for you.

The hydrate creator is new here, we haven’t created it yet. This is a good opportunity to think through what you may need to do for the hydrate action. So take a few minutes to try that out yourself and then come back.

Back? How did it go? You should end up with something like this in your action-types.js file:

const TYPES = {
export default TYPES;

Notice that we’ve added the HYDRATE action type here.  Then in your actions.js file you should have something like this:

export const hydrate = products => {
 return {
   type: HYDRATE,

Now that you have that good to go, I want to take a bit of time here to let you know about some important things to remember with resolvers:

The name of the resolver function must be the same of the selector that it is resolving for.

Notice here that the name of this resolver function is getProducts which matches the name of our selector.

Why is this important?

When you register your store, wp.data internally is mapping selectors to resolvers and matches on the names. This is so when client code invokes the selector, wp.data knows which resolver to invoke to resolve the data being requested.

The resolver will receive whatever arguments are passed into the selector function call.

This isn’t as obvious here with our example, but let’s say our selector was getProduct( id ) our resolver will receive the value of id as an argument when it’s invoked. The argument order will always be the same as what is passed in via the selector.

Resolvers must return, dispatch or yield action objects.

Resolvers do not have to be generators but they do have to return (or yield, if generators) or dispatch action objects. If you have need for using controls (to handle async side-effects via control actions), then you’ll want to make your resolver a generator. Otherwise you can just dispatch or return action objects from the resolver.

At this point you may have a few questions:

  • How does the client know what selectors have resolvers?
  • Can I detect whether the selector is resolving? How? 
  • Is the resolver always invoked every time a selector is called?

Before answering these questions, I think it’ll help if I unveil a bit of what happens in the execution flow of the control system in wp.data and the implementation of generators in resolvers.

So let’s breakdown roughly what happens when the getProducts selector is called by client code using the following flow chart:

A flowchart describing the execution flow with resolvers in wp.data.

Let’s step through this flowchart. When you call a selector, the first thing that happens is the selector returns it’s value. Then asynchronously, some wp.data logic also checks to see if there is a related resolver for the selector. If there is a resolver, then some internal logic will be used to determine whether or not resolution has started yet, or has finished.

Let’s pause here for a couple minutes and jump into a little side trail about resolution state.

Having a resolver that handles side-effects (usually asynchronously retrieving data via some sort of api) introduces a couple problems:

  • How do we keep the resolver logic from being executed multiple times if it’s initial logic hasn’t been completed yet (very problematic if we’re making network requests)?
  • How do we signal to client code that the resolver logic has finished?

In order to solve these problems, wp.data automatically enhances every registered store that registers controls and resolvers with a reducer, and a set of selectors and actions for resolution state. This enhancement allows wp.data to be aware of the resolution state of any registered selectors with attached resolvers.

The resolution state keeps track by storing a map of selector name and selector args to a boolean. What this means is that each resolution state is tied not only to what selector was invoked, but also what args it was invoked with. The arguments in the map are matched via argument order, matching primitive values, and equivalent (deeply equal) object and array keys (if you’re interested in the specifics, the map is implemented using EquivalentKeyMap).

With this enhanced state (which is stored in your store state on a metadata index), you have the following selectors (for usage examples, we’ll use our getProducts selector as an example) :

getIsResolvingReturns the raw isResolving value for the given selector and arguments. If undefined, that means the selector has never been resolved for the given set of arguments. If true, it means the resolution has started. If false it means the resolution has finished.wp.data.select( ‘data/products’ ).getIsResolving( ‘getProducts’ );
hasStartedResolutionReturns true if resolution has already been triggered for a given selector and arguments. Note, this will return true regardless of whether the resolver is finished or not. It only cares that there is a resolution state (i.e. not undefined) for the given selector and arguments.wp.data.select( ‘data/products’ ).hasStartedResolution( ‘getProducts’ );
hasFinishedResolutionReturns true if resolution has completed for a given selector and argumentswp.data.select( ‘data/products’ ).hasFinishedResolution( ‘getProducts’ );
isResolvingReturns true if resolution has been triggered but has not yet completed for a given selector and arguments.wp.data.select( ‘data/products’ ).isResolving( ‘getProducts’ );
getCachedResolversReturns the collection of cached resolvers.wp.data.select( ‘data/products’ ).getCachedResolvers();

You will mostly interact with resolution state selectors to help with determining whether an api request within a resolver is still resolving (useful for setting “loading” indicators). The enhanced resolution logic on your store will also include action creators, but typically you won’t need to interact with these much as they are mostly used internally by wp.data to track resolution state. However, the resolution invalidation actions here can be very useful if you want to invalidate the resolution state so that the resolver for the selector is invoked again. This can be useful when you want to keep the state fresh with data that might have changed on the server.

As with the selectors, the action creators receive two arguments, the first is the name of the selector you are setting the resolution state for, and the second is the arguments used for the selector call.

Action CreatorDescriptionExample Usage
startResolutionReturns an action object used in signalling that selector resolution has started.wp.data.dispatch( ‘data/products’ ).startResolution( ‘getProducts’ );
finishResolutionReturns an action object used in signalling that selector resolution has completed.wp.data.dispatch( ‘data/products’ ).finishResolution( ‘getProducts’ );
invalidateResolutionReturns an action object used in signalling that the resolution cache should be invalidated for the given selector and argswp.data.dispatch( ‘data/products’ ).invalidateResolution( ‘getProducts’ );
invalidateResolutionForStoreReturns an action object used in signalling that the resolution cache should be invalidated for the entire store.wp.data.dispatch( ‘data/products’ ).invalidateResolutionForStore();
invalidateResolutionForStoreSelectorReturns an action object used in signalling that the resolution cache should be invalidated for the selector (including all caches that might be for different argument combinations).wp.data.dispatch( ‘data/products’ ).invalidateResolutionForStoreSelector( ‘getProducts’ );

Now that we know about the resolution state, let’s return to the flowchart. We left off at the point where wp.data has determined there’s a related resolver for the given selector and is determining whether resolution has started yet or not. Based on what we just looked at, you should know how it does this. Right! It’s going to call a resolution state selector. The specific selector called here is hasStartedResolution. If that returns true, then wp.data will basically abort (because the resolver is already running, or completed asynchronously).

If hasStartedResolution( 'getProducts' ) returns false however, then wp.data will immediately dispatch the startResolution action for the selector. Then the getProducts resolver is stepped through.  Now remember getProducts is a generator. So internally, wp.data will step through each yield it finds in the resolver. Resolvers and controls are expected to only yield action objects or return undefined.

The first value yielded from the resolver is the fetch control action. Since wp.data recognizes that this action type is for a control it then proceeds to invoke the control.  Recognizing that the control returns a promise, it awaits the resolution of the promise and returns the resolved value from the promise via calling the generators generator.next( promiseResult ) function with the result.  This assigns the value to the products variable and the generator function continues execution. If there are no products, then the resolver returns undefined and this signals to the resolver routine that the resolver is done so wp.data will dispatch the finishResolution action for the selector.

If there are products, then the resolver returns the hydrate action. This triggers the dispatching of the hydrate action by wp.data and the dispatching of the finishResolution action because returning from a generator signals it is done. When the hydrate action is processed by the reducer (which we haven’t got to yet), this will change the state, triggering subscribers to the store, which in turn will trigger any selects in the subscriber callback. If the call to the getProducts was in subscribed listener callback, it will then get invoked and the latest value for getProducts (which was just added to the state) will get returned.

That in a nutshell (a pretty big nutshell at that), is how resolvers work!

Sidenote: if you want to dig into the technical logic powering this, check out the @wordpress/redux-routine package. This is implemented automatically as middleware in wp.data to power controls and resolvers, but can also be used directly as middleware for any redux based app.

WordPress Data Store Properties: Actions

This entry is part 5 of 15 in the series, A Practical Overview of the @wordpress/data API

In the previous post of this series, we took a brief interlude to take a look at an app that is using tree state to manage the data the app is using. We discovered that, while the app was functional, some potential problems with using state this way were beginning to surface. In this post, we’re going to start learning about the various data store properties (the properties of the options object we use when registering our store) and in the process we’re going to convert this app over to use wp.data!  For starters, we’re going to focus on the products data in our store, so let’s just leave the cart data alone for now.

If you’re more of a hands on person, you can participate in this conversion by going to this sandbox I’ve prepared. It’s the same app as what I introduced in the previous post except with a few differences:

  1. We’re going to wire things up to an external server and to do this we’ll use a free service called jsonbox.io.  I’ve set up a simple component to help wire things up to this service as described on the first page of the loaded app. This will become important later when we start working on communicating with this service for our app. jsonbox.io basically allows you to send and retrieve arbitrary data from it’s api.
  2. You’ll see in the src folder a data folder.  We’ll use this folder to keep our new data store in.
  3. Inside the data folder you’ll see a controls.js file. While we will be referencing and using the @wordpress/data-controls package later on, the apiFetch control in that package is too tightly coupled to the assumption of working with a WordPress REST endpoint. So I’ve gone ahead and created a control that works with the native window.fetch api. Don’t worry about this for now, we’ll see how it’s used later on.

You can go ahead and fork this sandbox so that you can make it your own as we work through the first property of the configuration object for registering our store.

Read more “WordPress Data Store Properties: Actions”

WordPress Data Interlude: Let’s talk Apps

This entry is part 4 of 15 in the series, A Practical Overview of the @wordpress/data API

In the last post I gave you a quick overview of the api for registering a new data store. I mentioned that this next post would take a closer look at the individual properties of a store, but before we do that, let’s take a brief break and take a look at an app that we’ll use to illustrate and practice the various concepts and apis around wp.data. The app we’re going to work with does the following:

  • A view that displays products in a product directory. Each product has an add to cart button for adding the product to a cart.
  • A view that displays the contents of the cart, the total for the items the cart, and a way to remove items from the cart.
  • A view for adding, editing and deleting existing products (it might serve as the admin view eventually).

So, go ahead and play around with this app I’ll be here when you get back.

You back? Great! The code portion of the app is here. Go ahead and take a few minutes to look at the code.

Now that you’ve taken a look at the code, let’s think about the design of the app for a few minutes. 

Read more “WordPress Data Interlude: Let’s talk Apps”

WordPress Data: How to Register a Custom Store

This entry is part 3 of 15 in the series, A Practical Overview of the @wordpress/data API

The primary way in which you interact with state in wp.data is through a registered store. In this post we’re going to give an overview of the api for registering a store and then subsequent posts will dive into the specific components of the store config.

registerStore( reducerKey, options )

Every data store found in wp.data has to be registered and you can see many examples of this throughout the Gutenberg codebase. The interface via which you register a store with wp.data is via the registerStore function. Through this function you add your store to the centralized data registry.

Sidenote: Something important to mention here is that when you register a store through wp.data.registerStore, you are registering with the default registry. This has some implications that we’ll get into a later article. In nearly all cases, it’s okay to register with the default store.

The registerStore function accepts two arguments: a name to identify the module (or store), and an object with values describing  how your state is represented, mutated (or modified), and accessed. 

The first argument can be referred to by many different terms, “store namespace”, “store key”, “store name”, “module name”, “module namespace”, or even “reducer key”! To keep things simple, we’ll call this argument the store name. It must be unique and a common pattern for naming data stores is to do something like group/store_content.  The group portion of the store name here might be your plugin name, or application name etc. The store_content portion of the store name here might be what kind of data is kept in the store.

Read more “WordPress Data: How to Register a Custom Store”

WordPress Data: Interfacing With the Store

This entry is part 12 of 15 in the series, A Practical Overview of the @wordpress/data API

Now that we have our new custom data store registered, we need to start integrating it with our example app. There are numerous ways in which you can interface with the data store in your components but I’m going to cover the four most common, straightforward ways.

withSelect( mapToProps )( Component)

withSelect is a higher order component that you use to connect your custom wp.data store with a component.

The argument you feed the curried function is a callback that maps values from selected state in wp.data to props that will then be fed through to the wrapped component. This callback you provide will receive two arguments: the first is select which is an interface to the wp.data registered stores, and ownProps which is the props that are passed into the component from parent components in the tree.

Here’s an example of withSelect being used:

Read more “WordPress Data: Interfacing With the Store”

WordPress Data: Putting it all together and registering your store!

This entry is part 11 of 15 in the series, A Practical Overview of the @wordpress/data API

To recap: so far in the series we’ve looked at the various properties of the configuration object for a store:

  • selectors: which interface with the store for retrieving data from its state.
  • actions: (actually action creators) which are used to dispatch action objects for handling by the reducer.
  • reducer: which receives dispatched actions and determines what happens to the state as a result.
  • controls: allowing for asynchronous side-effects on action generators or resolvers.
  • resolvers: linked to selectors, resolvers allow for automatically resolving data for the initial slice of state the selector is retrieving for.

Read more “WordPress Data: Putting it all together and registering your store!”

WordPress Data Store Properties: Controls

This entry is part 7 of 15 in the series, A Practical Overview of the @wordpress/data API

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" };
  // ensures things work with cors.
  options.credentials = "omit";
  return {
    type: "FETCH",

export default {
  FETCH({ path, options }) {
    return new Promise((resolve, reject) => {
        .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( 
            { 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.