The @wordpress/core-data package can be fairly intimiating. I spent a while a couple of months ago trying to wrap my head around it – especially the way side effects are managed via controls (and the accompanying concept of resolvers).

Whilst WordPress Core Data is moving away from this model and towards thunks, it’s likely the existing pattern of using controls to manage side effects will remain in the codebase for some time.

Therefore I felt it was high time I published some notes I took on the subject in the hope they will help someone else become more familiar with the package. These notes aren’t polished.

Controls

  • Can be thought of as “side effects” or “effects”.
  • Allow for asynchronous side-effects on:
    • action creators (specifically those which are generators)for dispatching.
    • resolvers – for selecting.
  • A control (or control function) defines the execution flow behaviour associated with a specific action type.
  • To create a control you:
    • define an action creator which returns an action object with a type of X.
    • define an entry on the controls object where the key matches X and the value is the control function you want to be invoked when the action is dispatched.
  • Rules for the object you specify as your controls value in your store:
    • each property should be 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.

Controls Example

For example we might want to persist some data to localStorage. As this is async we need controls.

First we define an action creator which we can dispatch from our components. This returns an object with the type PERSIST_BLOCKS_TO_STORAGE.

export function fetchBlocksFromStorage() {
	return {
		type: 'PERSIST_BLOCKS_TO_STORAGE',
	};
};

As we need async side effects for this action, then we define a control object (probably in control.js) as follows:

export default {
    ['PERSIST_BLOCKS_TO_STORAGE'](action) {
        return new Promise((resolve, reject) => {
            window.localStorage.setItem("someItem", serialize(action.blocks));
            resolve(action.blocks);
        });
    },
};

Notice how the key of the controls object entry (FETCH_BLOCKS_FROM_STORAGE) matches exactly with the action type property. This is how wp.data knows which controls object to run to manage side effects.

This function is then executed and the item is set to local storage.

Resolvers

  • Can be thought of as a companion to selectors.
  • Used for managing any async “flow” when getting data.
  • Typically defer to controls (see above) to do the actual data fetching – resolvers themselves only “manage” the execution flow.
  • When calling a selector
    • the value is returned immediately (synchronously).
    • then wp.data checks for related resolver.
    • then (based on resolution state) wp.data steps through the resolver logic.
    • any async side effects can be performed by yielding action control objects.
  • wp.data will step through each yield statement it finds in the resolver – yields should only be action objects.
  • Rules of resolvers:
    • The name of the resolver function must be the same as the selector that it is resolving for – when you register your store, wp.data is internally mapping selectors to resolvers and matches on the names
    • The resolver will receive whatever arguments are passed into the selector function call.
    • Resolvers must return, dispatch or yield action objects.

Resolvers Example

I might define the following selector:

export const getBlocks = ( state ) => {
	return state.present.blocks || [];
}

If this selector needs to fetch some data I will create a resolver with a matching name (i.e. getBlocks):

export function *getBlocks() {
    const rawBlocks = yield fetchBlocksFromStorage();
    const persist = false;
    const blocks = parse(rawBlocks);
    yield updateBlocks(blocks, persist);
    return blocks;
}

Notice how this resolver contains two yield statements. Let’s consider how these manage fetching data required for the resolver. fetchBlocksFromStorage is in fact an action creator which returns an action with a specific type:

export function fetchBlocksFromStorage() {
	return {
		type: 'FETCH_BLOCKS_FROM_STORAGE',
	};
};

This type matches a key on our controls object, the value of which is a function which fetches the data:

export default {
  ...
  [FETCH_BLOCKS_FROM_STORAGE]() {
    return new Promise((resolve, reject) => {
      const storedBlocks =
        window.localStorage.getItem("someItem") || [];
      resolve(storedBlocks);
    });
  },
};

The value of this function is what is yielded back to the resolver as rawBlocks.

For even such a simple example, I personally find the level of indirection difficult to manage. This is one reason I’m happy to see thunks in @wordpress/data.

Also note

  • 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.
  • Resolution Selectors:
    • getIsResolving
    • hasStartedResolution
    • hasFinishedResolution
    • isResolving
    • getCachedResolvers
wp.data.select( ‘data/products’ ).getIsResolving( ‘getProducts’ );
  • Resolution Action creators:
    • startResolution
    • finishResolution
    • invalidateResolution
    • invalidateResolutionForStore
    • invalidateResolutionForStoreSelector

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.