Using the WordPress JavaScript APIs for fun and profit part two - Introducing Redux

Welcome to part two of our series on using the WordPress JavaScript APIs. In this series, we explore the APIs that were introduced in WordPress 5.0. And, we'll look at how we can use them to better integrate with other plugins, in a reliable and safe way. Make sure to also check out our repository on GitHub, which contains all the code we'll be writing in this series.

In the previous post, we started creating a very basic form with a few fields. But, as you may have noticed, nothing exciting is happening just yet. Let's fix that! In this post, we’ll explain how we introduce Redux to the development stack and use it as our data store.

So, what's Redux?

As described on their own website, “Redux is a predictable state container for JavaScript apps". In this case, what is meant with 'state', is the current state of data within your application. Because you separate your data from your view (i.e. the form), you ensure your data isn’t contaminated with HTML or other data that shouldn’t be there. Additionally, by keeping track of the current and all previous states, Redux allows you to do all sorts of cool things, such as go back through time and replay previous actions in case of some kind of data corruption. It also allows you to centralize all your application data in one object, making it easier to share data across your entire application! This means that the bit of interoperability that we discussed in the first post, is easily achievable.

React-redux

React Redux is a package specifically created to make Redux and React integrate more easily. It thus takes away some of the nitty-gritty code that comes along when manually implementing Redux in your application. We’ll be utilizing this library later on in our React example.

What about jQuery?

So, in the previous post, we also made a jQuery version of the same form to illustrate how you can connect to the WordPress JavaScript libraries. However, using plain jQuery won't suffice once we start introducing Redux. To ensure we can use Redux, we'll have to also use a transpiled version of Redux via the power of Webpack and Babel, just like we do for the React version. The changes you’ll have to make to support the jQuery version of the code, is as follows:

  • In your webpack.config.js file, add jqueryApp: "./js/src/simple-form.js", to the entry object.
  • Edit index.html and change line 13 to <script type=“text/javascript” src=“js/dist/jqueryApp.js”></script>.

Prerequisites

If you haven't already, please make sure you check out the previous post on getting started with this project. This post covers adding the basic form that we'll be using throughout the project.

Setting up

Before we start introducing Redux to our application, there are a few add-ons we should install into our browser before starting. It's just to make our lives easier and to make it easier to see state changes happening in our application.

  • React Developer tools - This add-on allows you to debug React objects
  • Redux DevTools - This add-on lets you see state changes going on in your application while you're actively making changes, making it easier to debug in the process!

Please note: Before the Redux DevTools can be used, you need to add window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() to the createStore function (which you will see in the code samples later on) to ensure the Redux DevTool actually logs changes.

Next, lets add Redux to our project. Run yarn add redux and optionally run yarn add react-redux if you plan on implementing the React version of the form (or just check out chapter 2 from the repository). That’s all we need in terms of setting up. Time to code!

Adding Redux stores

The first step we'll be taking is to introduce a store to our project. A store is a single object that keeps track of the state of your application. To manipulate the state, you need to create and call an action. An action describes an event that took place within your application. The final thing to add will be a reducer, which interprets the action that occurred and translates it to something that transforms the state (i.e. updates it).

Defining actions

Before we can start creating and implementing a reducer, we first need to define some actions that our reducer can interpret and translate into a state change.

In the js/src/actions directory, create a file named form.js and add the following code:

export const SET_FORM_TITLE     = 'WPSJS_SET_FORM_TITLE';
export const SET_FORM_SLUG      = 'WPSJS_SET_FORM_SLUG';
export const SET_FORM_CONTENT   = 'WPSJS_SET_FORM_CONTENT';
export const SET_FORM_EXCERPT   = 'WPSJS_SET_FORM_EXCERPT';

/**
 * Sets the form title.
 *
 * @param {string} title The title to set.
 *
 * @returns {Object} The action.
 */
export function setFormTitle( title ) {
    return {
        type: SET_FORM_TITLE,
        title,
    };
}

/**
 * Sets the form slug.
 *
 * @param {string} slug The slug to set.
 *
 * @returns {Object} The action.
 */
export function setFormSlug( slug ) {
    return {
        type: SET_FORM_SLUG,
        slug,
    };
}

/**
 * Sets the form content.
 *
 * @param {string} content The content to set.
 *
 * @returns {Object} The action.
 */
export function setFormContent( content ) {
    return {
        type: SET_FORM_CONTENT,
        content,
    };
}

/**
 * Sets the form excerpt.
 *
 * @param {string} excerpt The excerpt to set.
 *
 * @returns {Object} The action.
 */
export function setFormExcerpt( excerpt ) {
    return {
        type: SET_FORM_EXCERPT,
        excerpt,
    };
}

The code we just added to the file is relatively simple. First, we're exporting a couple of constants that define the various action names that can be used in our application. After that, we defined several functions that describe a combination between the action names and what data they in turn 'write' to the action.

Overall, this is rather simplistic code. No dependencies, just generic JavaScript. Awesome!

The reducer

Time to add a reducer. As described in the previous section, the reducer interprets an action and can execute a variety of functions on the data before sending it off to the state. It’s possible to have multiple reducers that all listen in on the same actions and have them perform different tasks. But, in our case, we’ll only be needing a single reducer.

The first file we'll be creating is formData.js in the js/src/reducers directory and add the following code:

import { SET_FORM_TITLE, SET_FORM_SLUG, SET_FORM_CONTENT, SET_FORM_EXCERPT } from "../actions/form";

const INITIAL_STATE = {
	title:   '',
	slug:    '',
	content: '',
	excerpt: '',
};

/**
 * Reducer for the form data.
 *
 * @param {Object} state  The current state.
 * @param {Object} action The current action.
 *
 * @returns {Object} The new state.
 */
export default function formDataReducer( state = INITIAL_STATE, action ) {
	switch ( action.type ) {
		case SET_FORM_TITLE:
			return {
				...state,
				title: action.title,
			};

		case SET_FORM_SLUG:
			return {
				...state,
				slug: action.slug,
			};

		case SET_FORM_CONTENT:
			return {
				...state,
				content: action.content,
			};

		case SET_FORM_EXCERPT:
			return {
				...state,
				excerpt: action.excerpt,
			};
	}

	return state;
}

The above code will setup an initial state when there is none, and will listen for incoming actions. Based on the action, it will set a particular value in the state and return the state. That's it!

The next file we'll add isn't explicitly necessary, but is good practice when working with Redux, especially if you plan on adding more reducers over time. In the js/src/reducers directory, add a file called index.js and add the following code:

import { combineReducers } from "redux";
import formReducer from "./formData";

/**
 * Combines all reducers into a single one.
 */
export default combineReducers( {
    form: formReducer,
} );

You might be wondering why we decided to call this file index.js. The reason behind this is that, when we later import this file into our forms, we can simply import the reducers directory and that's it. Basically, if you use a file named index.js, that file becomes the 'entry point' for that directory, making bulk importing easier (when applied correctly). Please note that this approach isn’t explicitly necessary and you could just directly import the formReducerwithout combining them, but considering most applications end up having multiple reducers to handle various aspects of the application state, it’s best practice to combine them into a single reducer.

In Redux terms, this is what we call a 'root reducer'; A reducer that collects all other reducers and becomes the entry-point (i.e. the root) to all reducers within the application.

Integrating Redux into the form

Now that we've set up the Redux side of things, it's time to implement it into the forms we created in the previous post. This is the part that brings everything together and connects the frontend side of things with the state management side of this application.

React

In the simple-form-react.js, we'll be adding a lot of new code that will be handling the connection to the form and the store we just created. The resulting code is as follows:

import React from 'react';
import { connect } from "react-redux"
import { setFormContent, setFormExcerpt, setFormSlug, setFormTitle } from "./actions/form";

/**
 * Class that creates a simple input form.
 */
class BaseSimpleForm extends React.Component {

    /**
     * Constructs the base form.
     *
     * @param {Object} props The props to be used in the form.
     */
    constructor( props ) {
        super( props );

        this.onTitleChange   = this.onTitleChange.bind( this );
        this.onSlugChange    = this.onSlugChange.bind( this );
        this.onContentChange = this.onContentChange.bind( this );
        this.onExcerptChange = this.onExcerptChange.bind( this );
    }

    /**
     * Handles the changing of the title field.
     *
     * @param {Object} event The event that took place.
     *
     * @returns void
     */
    onTitleChange( event ) {
        this.props.setFormTitle( event.target.value );
    }

    /**
     * Handles the changing of the slug field.
     *
     * @param {Object} event The event that took place.
     *
     * @returns void
     */
    onSlugChange( event ) {
        this.props.setFormSlug( event.target.value );
    }

    /**
     * Handles the changing of the content field.
     *
     * @param {Object} event The event that took place.
     *
     * @returns void
     */
    onContentChange( event ) {
        this.props.setFormContent( event.target.value );
    }

    /**
     * Handles the changing of the excerpt field.
     *
     * @param {Object} event The event that took place.
     *
     * @returns void
     */
    onExcerptChange( event ) {
        this.props.setFormExcerpt( event.target.value );
    }

    /**
     * Renders the form.
     *
     * @returns {JSX} The rendered form.
     */
    render() {
        return (
            <div>
                <section>
                    <label htmlFor="title">Title</label>
                    <input type="text" id="title" value={ this.props.title } onChange={ this.onTitleChange } />
                </section>

                <section>
                    <label htmlFor="slug">Slug</label>
                    <input type="text" id="slug" value={ this.props.slug } onChange={ this.onSlugChange } />
                </section>

                <section>
                    <label htmlFor="content">Content</label>
                    <textarea name="content" id="content" cols="60" rows="10" onChange={ this.onContentChange }>{ this.props.content }</textarea>
                </section>

                <section>
                    <label htmlFor="excerpt">Excerpt</label>
                    <textarea name="excerpt" id="excerpt" cols="60" rows="10" onChange={ this.onExcerptChange }>{ this.props.excerpt }</textarea>
                </section>
            </div>
        );
    }
}

/**
 *  Maps the state to props.
 *
 * @param {Object} state The state to map.
 *
 * @returns {Object} The mapped props.
 */
const mapStateToProps = ( state ) => {
    return {
        title: state.form.title,
        slug: state.form.slug,
        content: state.form.content,
        excerpt: state.form.excerpt,
    };
}

/**
 * Maps actions to be dispatched, to props.
 *
 * @param {function} dispatch The dispatch function.
 *
 * @returns {Object} The dispatch functions mapped to props.
 */
const mapDispatchToProps = ( dispatch ) => {
    return {
        setFormTitle:   ( title ) => dispatch( setFormTitle( title ) ),
        setFormSlug:    ( slug ) => dispatch( setFormSlug( slug ) ),
        setFormContent: ( content ) => dispatch( setFormContent( content ) ),
        setFormExcerpt: ( excerpt ) => dispatch( setFormExcerpt( excerpt ) ),
    }
}

export default connect( mapStateToProps, mapDispatchToProps )( BaseSimpleForm );

We’ll start by discussing every function added in this new version of the form.

/**
 * Constructs the base form.
 *
 * @param {Object} props The props to be used in the form.
 */
constructor( props ) {
    super( props );

    this.onTitleChange   = this.onTitleChange.bind( this );
    this.onSlugChange    = this.onSlugChange.bind( this );
    this.onContentChange = this.onContentChange.bind( this );
    this.onExcerptChange = this.onExcerptChange.bind( this );
}

This constructor code sets up the parent class we're extending. It also binds the this property to the four methods we're adding that'll be handling the form fields, so we have access to the class' this property. We need to use this approach due to the way JavaScript scopes the reserved this keyword.

/**
 * Handles the changing of the title field.
 *
 * @param {Object} event The event that took place.
 *
 * @returns void
 */
onTitleChange( event ) {
    this.props.setFormTitle( event.target.value );
}

/**
 * Handles the changing of the slug field.
 *
 * @param {Object} event The event that took place.
 *
 * @returns void
 */
onSlugChange( event ) {
    this.props.setFormSlug( event.target.value );
}

/**
 * Handles the changing of the content field.
 *
 * @param {Object} event The event that took place.
 *
 * @returns void
 */
onContentChange( event ) {
    this.props.setFormContent( event.target.value );
}

/**
 * Handles the changing of the excerpt field.
 *
 * @param {Object} event The event that took place.
 *
 * @returns void
 */
onExcerptChange( event ) {
    this.props.setFormExcerpt( event.target.value );
}

The four methods we bound in the constructor, are defined here and define the events taking place within the form. At this point, all they do is call another method that is present in our props. More on that later.

/**
 * Renders the form.
 *
 * @returns {JSX} The rendered form.
 */
render() {
    return (
        <div>
            <section>
                <label htmlFor="title">Title</label>
                <input type="text" id="title" value={ this.props.title } onChange={ this.onTitleChange } />
            </section>

            <section>
                <label htmlFor="slug">Slug</label>
                <input type="text" id="slug" value={ this.props.slug } onChange={ this.onSlugChange } />
            </section>

            <section>
                <label htmlFor="content">Content</label>
                <textarea name="content" id="content" cols="60" rows="10" onChange={ this.onContentChange }>{ this.props.content }</textarea>
            </section>

            <section>
                <label htmlFor="excerpt">Excerpt</label>
                <textarea name="excerpt" id="excerpt" cols="60" rows="10" onChange={ this.onExcerptChange }>{ this.props.excerpt }</textarea>
            </section>
        </div>
    );
}

Compared to the previous version of the file, we've only added two things per input field:

  • The value property, which receives its value from the props.
  • The onChange property, which defines what to do when something changes in the field (i.e. someone types something).

Next comes the part where the magic happens and is made slightly easier thanks to react-redux, which we installed earlier!

/**
 *  Maps the state to props.
 *
 * @param {Object} state The state to map.
 *
 * @returns {Object} The mapped props.
 */
const mapStateToProps = ( state ) => {
    return {
        title: state.form.title,
        slug: state.form.slug,
        content: state.form.content,
        excerpt: state.form.excerpt,
    };
}

/**
 * Maps actions to be dispatched, to props.
 *
 * @param {function} dispatch The dispatch function.
 *
 * @returns {Object} The dispatch functions mapped to props.
 */
const mapDispatchToProps = ( dispatch ) => {
    return {
        setFormTitle:   ( title ) => dispatch( setFormTitle( title ) ),
        setFormSlug:    ( slug ) => dispatch( setFormSlug( slug ) ),
        setFormContent: ( content ) => dispatch( setFormContent( content ) ),
        setFormExcerpt: ( excerpt ) => dispatch( setFormExcerpt( excerpt ) ),
    }
}

export default connect( mapStateToProps, mapDispatchToProps )( BaseSimpleForm );

In the above section, there are three parts that we need to discuss.

First off, mapStateToProps is a helper function that maps data that exists within the state to props and thus makes it available for the component. Secondly, mapDispatchToProps binds the actions that we've created earlier and adds them to the props after which we utilize it in the bound methods, such as onTitleChange.
Last but not least, the connect function allows us to bind both these aforementioned functions to the form component, which we then export as its own component.

There's one last change that's necessary before we can consider our React version of this form as 'complete'. We need to make a few changes to the app.js file.

import React from 'react';
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import rootReducer from "./reducers";
import { createStore } from "redux";

import SimpleForm from "./simple-form-react";

const store = createStore(
    rootReducer,
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

class App extends React.Component {
    render() {
        return (
            <Provider store={ store }>
                <SimpleForm />
            </Provider>
        )
    }
}

ReactDOM.render( <App/>, document.getElementById( "root" ) );

We start by importing three parts into our file: The Provider component, our rootReducer and the createStore function from Redux.

After that, we define our store by calling createStore and pass in our root reducer. Next up, we wrap the form we added in the previous version, with the Provider component and pass in the store we just created as a property.

That's it! We just connected Redux to our form and all data is persisted within the lifecycle of our application.

jQuery

For the jQuery version of the form, we won't have to do much else than connect the reducer to the form and the input fields.

The end result we'll be going for, is as follows:

import rootReducer from "./reducers";
import { createStore } from "redux";
import { setFormContent, setFormExcerpt, setFormSlug, setFormTitle } from "./actions/form";

const store = createStore(
    rootReducer,
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

$( function () {
    // Attach events
    $( '#title' ).on( 'keyup', function() {
        store.dispatch( setFormTitle( this.value ) );
    } );

    $( '#slug' ).on( 'keyup', function() {
        store.dispatch( setFormSlug( this.value ) );
    } );

    $( '#content' ).on( 'keyup', function() {
        store.dispatch( setFormContent( this.value ) );
    } );

    $( '#excerpt' ).on( 'keyup', function() {
        store.dispatch( setFormExcerpt( this.value ) );
    } );

    // Subscribe to changes
    store.subscribe( function() {
        const state = store.getState();

        $( '#title' ).val( state.form.title );
        $( '#slug' ).val( state.form.slug );
        $( '#content' ).val( state.form.content );
        $( '#excerpt' ).val( state.form.excerpt );
    } );
} );

Let's go through the new additions in the code and discuss what's going on, to gain a better understanding of all the moving parts.

At the top of the file, we import all the necessary files and create the Redux store with the root reducer so it’s available in our form.

import rootReducer from "./reducers";
import { createStore } from "redux";
import { setFormContent, setFormExcerpt, setFormSlug, setFormTitle } from "./actions/form";

const store = createStore(
    rootReducer,
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

Beneath the form we made in the previous post, we attach the keyup event to the input fields. We also ensure that the correct function gets dispatched to the store, so that any changes that are made, are persisted to the store.

// Attach events
$( '#title' ).on( 'keyup', function() {
    store.dispatch( setFormTitle( this.value ) );
} );

$( '#slug' ).on( 'keyup', function() {
    store.dispatch( setFormSlug( this.value ) );
} );

$( '#content' ).on( 'keyup', function() {
    store.dispatch( setFormContent( this.value ) );
} );

$( '#excerpt' ).on( 'keyup', function() {
    store.dispatch( setFormExcerpt( this.value ) );
} );

Lastly, we subscribe to changes being made and update the input fields accordingly, by reading out the values available in the state. Basically, the form now reads from and writes to the state, via a simple and predictable way. You might be wondering why we’re setting the form field values, when the value is already present in the field when you type. The reason for this is that it ensures that that the form will properly reflect a changed value. Even if any other piece of code, such as a different plugin, alters something within the state.

Please note: We're referencing state.form here as that's how we named the reducer earlier on in our root reducer.

// Subscribe to changes
store.subscribe( function() {
    const state = store.getState();

    $( '#title' ).val( state.form.title );
    $( '#slug' ).val( state.form.slug );
    $( '#content' ).val( state.form.content );
    $( '#excerpt' ).val( state.form.excerpt );
} );

Conclusion

That's it! Now everything that you type in your form, will be persisted to (and read from) your store. This extra abstraction of data storage and collection will help us, later on, to connect to WordPress and read out data from other plugins and will allow us to add to it.

In the next post, we’ll be looking at how WordPress deals with this kind of data storage. We'll also see how we can hook into it to do some more interesting things.