1

Where does the Gutenberg store site and Block Editor state data, how do I access it, and how can I use it in block and sidebar components so that they will update and re-render automatically when it changes?

What is the difference between useSelect() and withSelect()?

1
  • // TODO: improve question, add links, verify example code is functional and illustrates the correct stuff
    – bosco
    Jun 25 at 0:24
2

✨ Intro to the Gutenberg Data Module ✨

Gutenberg's core data model shares a lot of the API and ideology of Redux, with a number of differences. Reading up on Redux can provide a lot of insight into how Gutenberg's data management functions.

This lecture will be on the final exam.


Application State Architecture

Block Editor application state is split up between a number of different "data stores" responsible for managing a specific scope. In addition to containing an object composed of the state data it is responsible for, each store provides a number of three foundational components:

  • Action Creators are functions which take in arguments, perform necessary work and leverage external side-effects like calling the REST API to shape that data into an event with a payload (an action), and trigger that event on the data store (dispatch the action).
  • Resolvers are usually "pure functions" without side-effects which listen for actions on the store, and process them to update the store's state data, if necessary.
  • Selectors are functions which retrieve data from the store's state, possibly shaping it into something more useful than the raw values in the process. In Gutenberg, selectors may call on action creators to populate missing state - more on that below.

Data Store Interface

In Gutenberg, we can access a store's public action creators by calling @wordpress/data's dispatch() function and passing it the name of the store (or the store's definition, once issue #27088 is closed). The object that it returns contains all the functions we can use to effect the store's state.

Similarly, we can acquire an object containing all of the functions we can use to retrieve state data from the store by passing it's name/definition to select():

import { select } from '@wordpress/data';

console.log( select( 'core/block-editor' ).isTyping() );

Some of Gutenberg's selectors will actually dispatch an action to retrieve data that's missing from the store when called, creating a notable situation where the first time you call the selector it returns undefined or whatever the initial value in state is, but another call moments later will return the expected data:

const logPublicPostTypes = () => console.log( select( 'core' ).getPostTypes( { public: true } ) );

logPublicPostTypes() // Logs `null`.

setTimeout( logPublicPostTypes, 1000 ); // Logs an array of post type objects.

The take-away here is that selectors provide synchronous access to data in state - even if they dispatch an action that will shortly alter the very state which they just returned.


Subscription

We can't always predict whether or not a store has the data we need or if a selector will have to dispatch an action to retrieve it for us. Fortunately, data stores provide a subscribe() function in order to attach callbacks which will be executed whenever an action is dispatched. In Gutenberg, they're only executed when the state actually changes.

@wordpress/data exposes a subscribe() function which will execute it's callback whenever any store's state changes. So we can use it to watch for asynchronously loaded data or specific state changes.

subscribe() also returns a function which can be called to remove the subscription, allowing us to detach functionality for which state updates are no longer relevant:

import { subscribe, select } from '@wordpress/data';

const { getPostTypes } = select( 'core' );

const unsub = subscribe(
  () => {
    const public_post_types = getPostTypes( { public: true } );

    if( ! public_post_types )
      return;
    
    doCoolThingsWithPostTypes( public_post_types );
    unsub();
  }
);

function doCoolThingsWithPostTypes( post_types ) {
  // ...
}

React Component Integration

As detailed above, the selectors used to access state data in stores only provide an immediate and synchronous snapshot of the stored value. So using a selector to retrieve data in a component would only give you the current value in state whenever something renders the component. This is usually not very useful - if nothing ever triggers a re-render for the component, then it's logic would never update to reflect the more recent state of the application. In the case of the state being asynchronously populated from the REST API, this might mean that the component never even processes the data which it depends on.

Both the useSelect() hook and the withSelect() utility are means to the same end - they provide a mechanism to expose state data from stores to a component and simultaneously create a subscription to re-render that component when that state data changes.

Which one you choose is largely a matter of personal preference, with the caveat that useSelect() can only be used inside of a functional component. For the most part however, hooks like useSelect() are widely considered to be a more elegant interface which solves a lot of the annoyances inherent to the more traditional "Higher-Order Component" pattern (see also this elaboration on hooks from the co-creator of Redux).

In my opinion, it's easier to understand the inner mechanism of HOCs, but hooks are easier to learn to use, produce more navigable and maintainable code, and mitigate a lot of "wrapper hell" pollution in the component hierarchy.

The withSelect() Utility

Counter-intuitively, "Higher Order Components" are not actually components, but rather functions which receive a component as an argument and wrap additional components and functionality around it. Sometimes the name is also applied to a sort of "curried HOC", where a function does not directly take in a component as an argument and returned a wrapped version of it, but rather returns a new function which does. This describes @wordpress/data's withSelect() and well as Redux's connect().

Basic Usage

withSelect() receives a single argument - a callback function which receives @wordpress/data's select() function and a component's provided props as arguments, and is expected to return either an object containing component props derived from selectors or undefined. Internally, withSelect() subscribes to the relevant stores - so the callback function is executed to re-calculate the mapping whenever state changes.

The HOC which withSelect() returns can then be used to add those selector-derived props to a component. When the values in the mapping change due to the internal store subscription, the component's props change and trigger a re-render with the new data.

So we can use withSelect() in order to create a custom HOC which will populate a postTypes prop for a component:

const withPublicPostTypes = withSelect(
  ( select ) => ( {
      postTypes: select( 'core' ).getPostTypes( { public: true } ),
  } )
);

const PostTypeList = ( { title = 'Post Types', postTypes = [] } ) => (
  <div>
    <h3>{title}</h3>

    <ul>
      {postTypes.map( type => (<li key={type.slug}>{type.name}</li>) )}
    </ul>
  </div>
);

const PublicPostTypeList = withPublicPostTypes( PostTypeList );
Using Props

In addition to re-calculating the property map on on state changes, it will also recalculate when the props received by the HOC's wrapper component change. So we can even expose a new postTypeArgs prop and translate it into a postTypes prop for the wrapped component (using the same PostTypeList component from above):

const withPostTypes = withSelect(
  ( select, ownProps ) => {
    const { getPostTypes } = select( 'core' );
    const { postTypeArgs = {} } = ownProps;

    return {
      postTypes: getPostTypes( postTypeArgs ),
    };
  }
);

const QueryablePostTypeList = withPostTypes( PostTypeList );
<QueryablePostTypeList
  title="Public Post Types"
  postTypeArgs={{ public: true }}
/>

The useSelect() Hook

React Hooks (not be confused with WordPress's PHP action/filter hooks) are a relatively new addition to the React API, and serve as a pattern to more elegantly expose various functionality for use inside of functional components (components written as functions rather than classes). Prior to hooks, there was not an easy method to use state or attach functionality concerned with a component's lifecycle when using the more succinct functional component syntax.

Basic Usage

useSelect() takes in a callback function which is given the select() function and expected to return a value derived from selectors, similar to the callback which is passed to withSelect(). We might implement the <PublicPostTypeList> component from the last section using the useSelect() hook as follows:

const PublicPostTypeList = ( props ) => {
  const postTypes = useSelect( ( select ) => select( 'core' ).getPostTypes( { public: true } ) );

  return <PostTypeList postTypes={postTypes} ...props />;
}

Now, postTypes will always have the latest value of the public post type list from the store. If that list changes (like if that state was not populated before this component's first render), useSelect()'s internal subscription will trigger a re-render automatically.

Dependency Argument & Memoization

The second argument to useSelect() is an array of dependency values. If specified, then after the first render, useSelect() will only execute our mapping callback if one or more of the values in that array change, and will otherwise provide the memoized (cached) value from the previous run. This is a useful optimization for cases where the values derived from selectors factor in props or other external values, and you are not concerned with changes in state unless those values have changed, too:

const QueryablePostTypeList = ( { postTypeArgs = {}, ...props } ) => {
  const postTypes = useSelect(
    ( select ) => select( 'core' ).getPostTypes( postTypeArgs ),
    [ postTypeArgs ]
  );

  return <PostTypeList postTypes={postTypes} ...props />;
}

Custom Hooks

Like all other hooks, useSelect() can also be used in composing a custom hook in order to create reusable functionality from multiple hooks or to provide a simplified interface:

const useRecentPostsOfTypes = ( args = {} ) => useSelect(
  ( select ) => select( 'core' ).getPostTypes( args ),
  [ args ]
);

const PublicPostTypeList = ( props ) => {
  const postTypes = usePostTypes( { public: true } );

  return <PostTypeList postTypes={postTypes} ...props />;
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.