WordPress Data Store Properties: Resolvers

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);
 }
 return;
}

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 = {
 UPDATE: "UPDATE",
 CREATE: "CREATE",
 DELETE: "DELETE",
 HYDRATE: "HYDRATE"
};
 
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,
   products
 };
};

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) :

SelectorDescriptionUsage
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.

Series NavigationWordPress Data Store Properties: ControlsWordPress Data Store Properties: Action Creator Generators
  1. Great series! =)

    Do you have some suggestion to check the loading state for the actions? Maybe something similar to the getIsResolving from the resolvers.

    1. Currently wp.data doesn’t have anything automatically built in to track loading state for actions, but actions do return a promise, so you can use that to derive loading state. You just have to be careful that if you are waiting for the promise resolution within a useEffect that you aren’t setting state after the promise resolves and when the component is rendering.

      Alternatively you can create your own state for tracking loading state. In WooCommerce we do that.

Leave a Reply to renathoc Cancel reply

Up Next:

WordPress Data: Interfacing With the Store

WordPress Data: Interfacing With the Store