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() {
 // the dummyjson.com service returns paginated results. For the purpose of this demo
  // we only need 10 hence the limit param.
  const products = yield fetch(getResourcePath() + "?limit=10");
 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 dummyjson API. It should be in the products/utils.js folder already for you. Note, in the above example I’ve also added a limit parameter since we don’t need the full list of products the service returns.

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.

  2. Hi Darren, great series (even if it’s still a lot over my head for now). How does the frontend wp.data resolve or fetch existing data from the WP database?

    And how can you add something custom to the API response? For example, in one of my plugins I’ve replaced the taxonomy panel with a custom panel. So I get the default taxonomy response with what looks like /wp/v2/post_tag https://github.com/WordPress/gutenberg/blob/master/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js#L229 (which maybe answers my first question?)

    If you are fetching a taxonomy, how would you go about including something custom in the response? For example, like taxonomy meta or in my case a boolean that’s editable via filter?

    1. Hi Kathy,

      Sorry for the delay in getting back to you.

      How does the frontend wp.data resolve or fetch existing data from the WP database?

      It all depends on what stores are registered in the frontend and whether you have access to them (either via enqueuing or making sure your script loads after the store you want to use is registered).

      And how can you add something custom to the API response?

      There’s a couple components to this:

      1. You’d have to register your custom data with the REST API endpoint (if that’s possible for the endpoint you want to access). You can see some info on that here
      2. The registered store resolves would have to expose your custom data. If they don’t, then you’d have to register your own store and use it. In the case of taxonomy it looks like the store exposes what’s on the taxonomy object from the REST source (not verified)?

      A handy tip you can use when finding out what’s available is that you can use the wp.data global directly in your browser console on any page that registers stores. So for example, if I open up my browser console on a post editor screen, I can do this and get a list of all the available selectors in the registered core store:

      wp.data.select('core');
      

      I can also call a selector and find out what the response is. Caveat, if you call a selector attached to a resolver, the first call initiates the request. So you will need to call it again after the request is complete to get the response from the request.

      This is a great way to experiment and find out what’s exposed via the registered store.

  3. Hi, Thank you so much for this series of posts!

    I faced a situation and found a solution that works, but I’m wondering if I’m violating something by doing so.

    I wanted my resolver to check if the data is already in the state before yielding the action that makes the API request. So if I already have the data in the initial state, no api request is needed.

    Here’s what I did

    *getConnectionStatus() {
    const existingStatus = select( STORE_ID ).getConnectionStatus();
    if ( existingStatus.hasOwnProperty( 'isRegistered' ) ) {
    return existingStatus;
    }
    // make api request...

    Note that I invoke the same resolver. I understand it will return the value it finds in the state and won’t trigger the resolver again because it knows it’s already running.

    Does it look ok? Is it fine to use select from inside a resolver?

    1. Howdy! I’m not sure I understand why you need to do this though. Resolvers are intended to provide a mechanism for initially updating the state relevant to the selector if it has never been resolved yet. Once resolved, the resolver is never called again unless the resolution state is changed.

      Are you prehydrating the state somewhere else?

      1. Are you prehydrating the state somewhere else?

        Yes, via the initial state.

        The goal is to give this option to the consumer my package. If the information is not provided in the initialState for any reason, the store will look for it. But if the info is already in the state, no need for a api request.

        1. Okay, then in that case, whatever is doing the hydration should dispatch a finishResolution action with the appropriate arguments to let the store know that the state is resolved. Then when the selector is called it won’t hit the resolver.

          1. Thank you Darren for you replies.

            The thing is the value is informed via the initalState option when the store is created. So there’s no event to trigger the finishResolution method.

            initialState is passed as an option to createReduxStore. So, in this case, the resolver will serve as a fallback in case the initial state is missing this information.

            Sorry if I didn’t make myself clear before.

          2. In any case, we will trigger the finishResolution right after the store is created. I think it’s a more elegant solution. I was just wondering what the best practices were

          3. The thing is the value is informed via the initalState option when the store is created. So there’s no event to trigger the finishResolution method.

            Gotcha, I’m basing my reply on the assumption that the hydration is still coming from the server.

            I still think that it’d be best to keep complicated logic out of the resolver for knowing what the hydration state is (it shouldn’t really care) and work with how wp.data is architected. With that in mind, instead of hydrating via the initialState, what about creating an compound action explicitly for initial hydration of the state? So the logic in the action is roughly:

            “`
            export function* hydrateStore(
            initialState
            ) {
            // whatever the action is that your resolver uses to save state to the store.
            yield receiveState( initialState );
            // yield ‘finishResolution’ action (sorry can’t remember the syntax offhand).
            }

    2. Just pushing an update here, looking at the source code, found that your resolver could be a function or object, if you pass an object, you could add isFulfilled key as function, that will receive the state and could check against it to allow or not the resolver call:

      Reference:

      https://github.com/WordPress/gutenberg/blob/trunk/packages/data/src/redux-store/index.js#L367-L369

      https://github.com/WordPress/gutenberg/blob/trunk/packages/data/src/redux-store/index.js#L395

Leave a Reply to renathocCancel reply

Up Next:

WordPress Data: Interfacing With the Store

WordPress Data: Interfacing With the Store