Rolling Our Own Redux: Going Gutenberg Chapter VII

Using Redux Directly in the Gutenberg Block Editor

Going Gutenberg is an ongoing series about the process of integrating our products with the WordPress block editor. View all chapters in the series archive, and be sure to follow us on Facebook or Twitter to never miss an update.


One of the key promises of the new block editor coming in WordPress 5.0 (a.k.a. “Gutenberg”) is that its use of more contemporary JavaScript will push WordPress forward as a whole. From the Gutenberg Handbook itself:

Aside from enabling a rich post and page building experience, a meta goal is to move WordPress forward as a platform. Not only by modernizing the UI, but by modernizing the foundation.

React and Redux are cornerstone technologies of the block editor, and are essential to this vision of moving WordPress forward. WordPress makes it a bit easier to work with these technologies by providing some helper functions and custom ways of interacting with Redux. While these “helpers” will surely suit many projects and developers well, we’ve found it best to work with React and Redux directly.

To understand some of those decisions, we first need to understand the new way of centralizing UI data in the block editor: stores.

What’s a Store?

We’re not talking about Walmart here. In the context of modern web technologies, a store is a centralized location where structured data can be stored and referenced. Stores make managing the state of an application significantly easier, especially as applications become larger and larger—WordPress’ new block editor is a great example of their utility.

In the block editor, the store centralizes all data associated with the UI. Any change to the UI is applied directly to the store first, then reflected in the UI. This allows for easier management of state as myriad things are dragged, dropped, edited, and removed; it also allows for a centralization of the logic for creating and updating blocks.

The Block Editor’s Approach to Stores

The block editor currently provides a data package that’s in charge of handling the store and utilities around the store. This is essentially a wrapper for the default Redux store and is used heavily by the block editor for most of the data associated with the post and the editor state—everything from the post status and whether comments are allowed, to the entire configuration of blocks and their data in the post. Besides merely providing a specific store, the block editor’s data package also provides a series of higher-order functions to abstract common mechanisms (like getting and setting data or performing async calls to internal and external APIs).

Moving Away from the Block Editor’s Redux Wrapper

The block editor’s wrapper for the Redux store is great, but for our needs, we found that just using the default Redux store directly made more sense. Some of the main reasons:

Middleware
One of the major benefits of the store is the usage of middleware, with which you can extend or manage side effects for the data before it’s saved into the store. At the moment, this is handled in Gutenberg via resolvers, but when we use the default Redux store, we can more easily test and ensure side effects are applied in the correct order when actions are dispatched.
No Reinventing the Wheel
The React and Redux communities have generated many excellent data management patterns over the years. The block editor’s Redux wrapper deviates from some of the more common patterns, which makes sense for a number of things the block editor itself has to do. But for our events-related blocks, we’ve found it a bit easier to work with the more common Redux patterns.
Performance and Architecture
In addition to our block editor compatibility for The Events Calendar, we have several other plugins that will have blocks of their own. The ideal is for each new block type to plug new data into the store dynamically, which allows us to load only the JavaScript necessary for each specific block. This process is easier to pull off using Redux directly instead of the Gutenberg wrapper.
Documentation
The React and Redux communities have not only generated many excellent patterns for us to reuse—they’ve generated tons of great documentation, too. The actual code for the block editor Redux wrapper is well-documented, but the rest of the technical docs are lacking. By using Redux directly, we’ve been able to take advantage of a wealth of third-party technical docs to save our team time and onboard new members quickly.
Async Calls
At the moment, async calls are done via resolvers in Gutenberg (the same thing mentioned above). You can, however, reach a point where the number of requests or external async calls is large enough that it doesn’t fit correctly in a single object; the complexity and overall understanding of what’s going on can quickly become a nightmare to test, let alone to maintain. With the default Redux store, we can use sagas or thunks, which make things easier to follow and test.

For the amount of client-side data our events blocks have to manage, working with Redux directly offers much more control, testability, and adherence to Redux/React best practices.

Implementing Redux in a Gutenberg Block

There are two main concerns when using Redux directly in your blocks instead of using Gutenberg’s own store mechanism:

Concern #1: Entry Point
Usually, when you have a React app and you want to connect to the Redux store, you only have a single entry point through which the entire application can access the store via context. But with Gutenberg, each block is isolated and has to have its own entry point.
Concern #2: State Population
With most React apps, you can generate the initial—or “store”—state with server-side rendering or by saving the data in localStorage. Since Gutenberg blocks can have data from HTML attributes or meta fields, pulling the store state from the server or localStorage can mean a block’s initial state is out of sync with the current block state on page load.

Our Approach to Entry Points

A major benefit of React is the fact that components can be composed and enhanced via higher-order components. With this type of mechanism, we can group logic that is common in order to keep things DRY (“don’t repeat yourself”) while also offering an easier way to enhance components with more behaviors later on.

First, we need to create the store using Redux. What follows is a series of code examples demonstrating how we do this and other things—but please note that these examples are just excerpts. For full working source code, visit the events-gutenberg repository on GitHub.

export const initStore = () => {
    store = createStore( reducers, applyMiddleware( ...middlewares ) );
};

export const getStore = () => {
    return store;
};

Now we can call initStore() on our main file in order to create the store. After that point, we can call getStore() to give us access to the store. So the HOC (higher-order component) used to connect a block to the store with this approach looks as follows:

import { getStore } from 'data';
// withStore
export default ( additionalProps = {} ) => ( WrappedComponent ) => {
    const WithStore = ( props ) => {
        const extraProps = {
            ...additionalProps,
            store: getStore(),
        };

        return ;
    };
    return WithStore;
};

This HOC accepts a single argument called additionalProps, which is an object used to inject additional properties into the original component if required. It also injects the store property into the component, so if your block has a store property, be aware that this is overriding that value.

In summary, the following will be injected into the enhanced component:

  1. The original properties of the component
  2. New, additional properties if any
  3. A new store property with a reference to the data store

In order to connect the store with a block, you can rework your block declaration along the lines of the following code:

const EventWebsite = compose(
    withStore(),
    connect(
        null,  // mapStateToProps if any
        null, // mapDispatchToProps if any
    ),
    withSaveData(),
)( EventWebsite );

First, define a container or variable that enhances the block itself with the new HOC withStore(), and use connect() to inject properties and actions into the component. If required, there’s a third HOC, withSaveData(), which we’ll discuss further below.

Let’s say you have a container declaration where EventWebsite is a React component that represents the JSdom structure of your block. In order to enhance this component, you create a variable that uses the compose() function to create a new React component that executes every single one of the functions or HOC into the EventWebsite component.

After this point, in your block declaration object you can just call this variable and pass it as the value of the edit property of the block description (since edit is used to describe the structure of the block in the editor).

const block = {
    id: 'event-website',
    title: __( 'Event Website', 'events-gutenberg' ),
    icon: Icons.TEC,
    category: 'tribe-events',
    keywords: [ 'event', 'events-gutenberg', 'tribe' ],

    supports: {
        html: false,
    },

    edit: EventWebsite, // component enhanced and connected to the store

    save( props ) {
        return null;
    },
};

With this code, the component is connected with the store and should be able to use things like connect() to inject props using selectors or actions into the component.

Managing State Population

The next major goal is to consolidate the attributes as properties into the block, and to save those properties back into the component so that they’re saved into the database via WordPress. This requires making another higher-order component that will use a Gutenberg-provided function, setAttributes().

export default ( selectedAttributes = null ) => ( WrappedComponent ) => {
    class WithSaveData extends Component {
            keys = [];
            saving = null;

            constructor( props ) {
                super( props );
                this.keys = this.generateKeys();
            }

            generateKeys() {
                if ( isArray( this.attrs ) ) {
                    return this.attrs;
                }

                if ( isObject( this.attrs ) ) {
                    return keys( this.attrs );
                }

                console.warn( 'Make sure attributes is from a valid type: Array or Object' );

                return [];
            }


            componentDidMount() {
                const { setInitialState, attributes = {}, isolated, onBlockCreated } = this.props;

                setInitialState( {
                    ...this.props,
                    get( key, defaultValue ) {
                        return key in attributes ? attributes[ key ] : defaultValue;
                    },
                } );
            }

            componentDidUpdate() {
                const diff = this.calculateDiff();

                if ( isShallowEqual( this.saving, diff ) ) {
                    return;
                }

                this.saving = diff;
                if ( isEmpty( diff ) ) {
                    return;
                }
                this.props.setAttributes( diff );
            }

            calculateDiff() {
                const attributes = this.attrs;
                return this.keys.reduce( ( diff, key ) => {
                    if ( key in this.props && ! isShallowEqual( attributes[ key ], this.props[ key ] ) ) {
                        diff[ key ] = this.props[ key ];
                    }
                    return diff;
                }, {} );
            }

            get attrs() {
                return selectedAttributes || this.props.attributes || {};
            }

            render() {
                return ;
            }
    }

    return WithSaveData;
};

This HOC is based on an argument called selectedAttributes, an object used to extract the keys of the attributes and generate a diff between the original attributes of the block and its top-level properties. Every component that uses this HOC is required to define the same properties at the top level via mapStateToProps, which injects the new values into the component when the values in the store are updated.

Every time the top-level properties are updated by mapStateToProps, the componentDidUpdate life cycle is called. At this point, we do a diff between the new properties’ values and the attributes’ values, so any changes here mean the attributes should be updated. If that’s the case, we do a call to Gutenberg’s setAttributes() function, which is used to persist the attributes back into the block.

We need to make sure, however, that every time the blocks are mounted into the DOM we’re moving the initial attributes’ values into the store. For this, we have a function called setInitialState() that is fired every time the block is mounted so we can send the initial attributes’ values back into the store.

Next Steps

With this setup, we can now add sagas and use code splitting to add custom reducers during runtime. These steps help decrease the bundle size delivered to the clients, as we have a “normal” store that accepts all the goodies from the different React patterns provided by the community.

Being able to explore more React patterns will hopefully help us work through some of our remaining issues. An example would include handling one piece of data that needs to be propagated across multiple blocks—somethiWng we run into often with our Event block. Neither communicating with the server nor using localStorage are an ideal solution (for reasons elaborated above), so while setInitialState() works for us now, we’re confident we can find better solutions as our Gutenberg work continues.

Conclusion

A lot of effort and tinkering has gone into figuring out the best approaches using the Redux store directly and things are still subject to change. That said, it’s very likely our plugins, the plugins of many others, and even the new editor itself will remain a mix of both custom WordPress approaches and more traditional/widespread React patterns.

If you’re interested in following along with the work we’re doing, check out our Events Gutenberg extension on GitHub—feel free to read the code, play with the extension itself, and report any issues you stumble across.

For more news about all things Gutenberg and The Events Calendar, sign up for our newsletter.

  • Hidden

Going Gutenberg Series

Don’t forget to check our amazing Premium Plugins as well!