4

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
    Commented Jun 25, 2021 at 0:24

1 Answer 1

18

✨ Intro to the Gutenberg Data Module

Gutenberg's core data model shares a lot of the API and ideology of the state-management library 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. See the Data Module Reference in the Block Editor Handbook for an overview of each store.

Each contains a private, externally inaccessible object representing it's scope of application state. I addition to the state object, each store is also composed of a number of three foundational components (among other things):

  • Action Creators are functions which take in arguments, perform necessary work to shape those arguments into an event with a payload (an Action), and trigger that event on the data store ("dispatch the Action"). Action Creators can leverage external side effects in the process of creating their respective Actions - this includes things like querying the REST API for data. Action Creators are exposed externally, so we may call upon them to affect desired changes in application state.
  • Reducers listen for actions dispatched on their store and process them to update the store's state data (if necessary). They are externally inaccessible - so once a store has been constructed, we can no longer add to or modify the way in which a data store responds to dispatched actions. Reducers are also "pure functions," or functions which do not depend on external side-effects. Given the same arguments, a pure function will always return the same return value; or in our case, given the same action, a Gutenberg Reducer will always modify the store's state through the same, predictable logic.
  • 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 - that's an important point that we'll get into later.

To summarize, Action Creators effectively enable us to write changes to application state, and Selectors enable us to read it. Actions dispatched by Action Creator functions are consumed by Reducers which actually modify the state object - but both Actions and Reducers are generally externally inaccessible on the core data stores, and only really relevant if you are creating your own data store to manage the application state of a theme or plugin.

Developer Tools

The Redux DevTools Chrome extension is an invaluable tool for understanding application state in a Redux-powered application (or Redux-compatible application as it were). It will allow you to inspect the state object stored in Gutenberg stores as well as the sequence of dispatched actions and how they modify state over time, among many other useful features.


Data Store Interface

Action Creators

In Gutenberg, we can acquire an object containing all of a store's public Action Creators by passing the name (or definition object) of a store to @wordpress/data's dispatch() function. This is essentially our means to "write" to that store's state.

Selectors

Similar to Action Creators, 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 property's initial value in state may be), but another call moments later will return the expected data:

import { select } from '@wordpress/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

As demonstrated above, we can't always predict whether a Selector will return the data that we immediately need (such as the isTyping() Selector above), or if it might dispatch an Action to retrieve that data and update the store's state after the fact (as might anything that depends on data retrieved from the REST API such as available post types). Fortunately, data stores provide a subscribe() function in order to attach callbacks which will be executed whenever an action is dispatched on the store. Better yet, in Gutenberg they're only executed when the state actually changes.

@wordpress/data exposes a global subscribe() function which will execute it's callbacks 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 only gives you the current value in state whenever the component renders. What if you want the React component to re-render with the most recent value whenever the value changes in state?

Both the useSelect() hook and the withSelect() "Higher-Order-Component" 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 the function does not directly take in a component as an argument and return a wrapped version of it, but rather returns a new function which satisfies this definition. This describes @wordpress/data's withSelect() and well as Redux's connect(); both take in a number of arguments which are not components, then return a function which satisfies the definition of a HOC - the returned functions take in a component as an argument and wrap additional functionality around it before returning the wrapped component.

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 usePostTypes = ( args = {} ) => useSelect(
  ( select ) => select( 'core' ).getPostTypes( args ),
  [ args ]
);

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

  return <PostTypeList postTypes={postTypes} ...props />;
}
3
  • great write up, wonder if you could elaborate how to use @wordpress/core-data outside of gutenberg, say in a custom react settings page. Commented Aug 25, 2022 at 21:49
  • Thank you! :) Custom data stores are something I haven't really explored to much depth yet - but I would really love to. I think they could play a super useful role in plugins moving forward - especially in exposing easy-to-consume APIs to other parties for further extension. Like, the Gravity Forms editor implemented on top of custom data stores would be amazing. If I can find some time to dig in and maybe sort out a nice isolated use-case, I'll try to get a post together in the next week or so
    – bosco
    Commented Aug 25, 2022 at 22:50
  • This answer is solid gold - thank you, this really helped cement some concepts for me, I wish a background primer like this was in the official Gutenberg docs! This answers the "how" and "why" better than just basic reference docs. Commented Sep 10 at 22:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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