Categories
Core

Tutorial: Adding React Support to a WooCommerce Extension

Starting with WooCommerce 4.0, the React-powered admin interface of WooCommerce Admin has been included as part of the core WooCommerce experience for developers and merchants. This inclusion opens up lots of options for developers who want to provide a more modern JavaScript-powered experience for the folks who rely on their extensions, but setting up an existing extension to take advantage of some of these advancements can be intimidating. In this post, we’ll walk through the steps involved in adding support to a simple extension for WooCommerce’s React-driven admin experience. Along the way, we’ll also explore a few concepts related to modern JavaScript development that might be new if you’re used to traditional PHP development in WordPress.

Prerequisites

For this tutorial, we’re going to be working with lots of JavaScript tools. If you haven’t already, make sure you have a recent version of both Node and Composer installed locally. You can download and install them by visiting the links below.

Note: Composer isn’t strictly required for adding React support to an extension, but WooCommerce Admin relies on it, so it’s worth having it installed.

Overview of Steps

Ultimately, adding React to an extension amounts to writing some modern JavaScript, running it through a few utilities that transform it into a browser-ready static asset, and having WordPress load it when your extension is activated. In this tutorial, we’ll break that process down into smaller steps and explain what we’re doing at each step along the way. Here’s what we’ll do:

  • Update your version control configuration to ignore the some of the supporting files that Node generates
  • Configure a Node workflow with a package.json file
  • Configure Webpack for combining, transpiling, and minifying your extension’s JavaScript
  • Create a src directory to organize the JavaScript files you write
  • Generate your build directory
  • Register the minified JavaScript in WordPress and enqueue them to load alongside the other admin scripts.
Workflow Tip

If you’re starting an extension from scratch, you can use the built-in extension generator included with WooCommerce Admin. It handles all of the setup we’ll cover below automatically. Just clone the woocommerce/woocommerce-admin repository, follow its setup instructions, and then run npm run create-wc-extension to set up a boilerplate template for your extension.

Steps

Update your version control settings

As we add React support to our extension, we’ll be generating lots of files in our project directory that we don’t want to place under version control. Update your project’s version control configuration to ignore these paths. If you’re using Git, you can view this sample .gitignore file to see which paths need to be added.

We’ll go ahead and commit these updates to our project’s repository before proceeding:

> git add .gitignore
> git commit -m 'Exclude unnecessary Node-related paths from version control.'

Add a package.json file to define your extension’s Node workflow

The package.json file is critical to any application that relies on Node.js and JavaScript packages. You can think of this file as a kind of manifest that captures information about your project’s dependencies, configuration, and overall workflow (as it relates to the JavaScript realm).

If you don’t already have a package.json file in your project root, Node will generate one for you:

> npm init

Node will prompt you to enter some basic metadata about your project. Feel free to answer these prompts, but it’s also okay to accept the defaults. Our objective for now is getting the file generated.

Once you have the package.json file generated, let’s tell Node about some development dependencies that we’re going to be using to streamline our workflow:

> npm install --save-dev @wordpress/scripts @woocommerce/eslint-plugin

This command tells Node to download and install the @wordpress/scripts and @woocommerce/eslint-plugin packages (and all of their dependencies) from NPM and to add their names to our project’s list of development dependencies. Once everything is installed, let’s open up our package.json file. You’ll notice these packages have been added to the devDependencies object.

Note: There should also be a package-lock.json file in our project root. This file is similar to the composer.lock file you might already have in your project. It keeps a running map for all of the dependencies listed in our project and their installed versions.

While we have our package.json file open, let’s take advantage of the @wordpress/scripts package we installed by using it to standardize and streamline our workflow a bit. Update the scripts object:

"scripts": {
        "build": "wp-scripts build",
        "check-engines": "wp-scripts check-engines",
        "check-licenses": "wp-scripts check-licenses",
        "format:js": "wp-scripts format-js",
        "lint:css": "wp-scripts lint-style",
        "lint:js": "wp-scripts lint-js",
        "lint:md:docs": "wp-scripts lint-md-docs",
        "lint:md:js": "wp-scripts lint-md-js",
        "lint:pkg-json": "wp-scripts lint-pkg-json",
        "packages-update": "wp-scripts packages-update",
        "start": "wp-scripts start",
        "test:e2e": "wp-scripts test-e2e",
        "test:unit": "wp-scripts test-unit-js"
    },

Each property of the scripts object corresponds to a terminal command that Node will run when we call the command with npm run [command]. We won’t go into detail about what these scripts do in this tutorial. If you’re interested in learning what they do, you can read their documentation here. What is important to note for now, however, is that by using these scripts, we’re adopting a shared tooling and a standardized workflow for developing modern JavaScript that are both used within the WooCommerce and WordPress communities.

Add a JavaScript linting configuration file

You may already be using something like PHP_CodeSniffer in your project to make sure your code adheres to certain conventions for formatting and style. We’re going to lint our JavaScript with a utility called ESLint. It’s highly configurable, which can be overwhelming when we’re just getting started. Luckily, the @woocommerce/eslint-plugin package we installed can simplify things for us quite a bit. Let’s create a file called .eslintrc in our project root. Inside that file, add the text below:

{
  "extends": [ "plugin:@woocommerce/eslint-plugin/recommended" ]
}

This is a simple directive that tells ESLint to use the recommended configuration in the @woocommerce/eslint-plugin package in our project’s development dependencies. This Woo-specific configuration primarily extends the @wordpress/eslint-plugin configuration but adds a few additional rules that apply to WooCommerce JavaScript conventions.

Add a Webpack configuration

One of the defining characteristics of modern JavaScript applications is the modularity with which they are written. Whereas it used to be common to encounter JavaScript that was disorganized and difficult to understand, modern JavaScript apps much more closely resemble their counterparts that are written in languages like PHP. To help create and maintain organized, modular JavaScript that is still easy to load in (most) web browsers, developers thankfully have access to bundler utilities that compile all of the modules in their application into a minified static asset that can be served efficiently. There are lots of these utilities out there, but we’re using one called Webpack for our extension. We didn’t explicitly install it, but it’s part of the @wordpress/scripts package we’re using for our development workflow tasks.

Let’s go ahead and create a file called webpack.config.js in our project root. Inside that file, we’ll build a configuration that tells Webpack how it should process the files in our project.

Just like we did with our ESLint configuration earlier, we can use a baseline configuration to handle a lot of foundational settings. Let’s import a few modules that make life easier:

const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
const DependencyExtractionWebpackPlugin = require( '@wordpress/dependency-extraction-webpack-plugin' );

Here we’re requiring the default Webpack configuration for WordPress and a dependency extraction plugin, both of which are included with the @wordpress/scripts package we installed when we set up our project’s development dependencies in package.json. You can find out more about what settings the default WordPress configuration includes by browsing its source code here.

We’re going to use the dependency extraction plugin to solve a challenge that is common in modular JavaScript applications: dependency sharing. This plugin lets us tell Webpack which dependencies our project expects to already be available in the environment and which, therefore, don’t need to be re-bundled into our minified code too. Let’s add the code below to our webpack.config.js file and take a look at what it does:

const requestToExternal = ( request ) => {
    const wcDepMap = {
        '@woocommerce/components': [ 'window', 'wc', 'components' ],
        '@woocommerce/csv-export': [ 'window', 'wc', 'csvExport' ],
        '@woocommerce/currency': [ 'window', 'wc', 'currency' ],
        '@woocommerce/date': [ 'window', 'wc', 'date' ],
        '@woocommerce/navigation': [ 'window', 'wc', 'navigation' ],
        '@woocommerce/number': [ 'window', 'wc', 'number' ],
        '@woocommerce/settings': [ 'window', 'wc', 'wcSettings' ],
        '@woocommerce/tracks': [ 'window', 'wc', 'tracks' ],
    };

    if ( wcDepMap[ request ] ) {
        return wcDepMap[ request ];
    }
};

const requestToHandle = ( request ) => {
    const wcHandleMap = {
        '@woocommerce/components': 'wc-components',
        '@woocommerce/csv-export': 'wc-csv',
        '@woocommerce/currency': 'wc-currency',
        '@woocommerce/date': 'wc-date',
        '@woocommerce/navigation': 'wc-navigation',
        '@woocommerce/number': 'wc-number',
        '@woocommerce/settings': 'wc-settings',
        '@woocommerce/tracks': 'wc-tracks',
    };

    if ( wcHandleMap[ request ] ) {
        return wcHandleMap[ request ];
    }
};

We’re creating two request-handling functions—one that maps our dependencies to their global variables in the WordPress application space and one that maps our dependencies to the handles that are used to enqueue them via wp_enqueue_script(). The @wordpress/dependency-extraction-plugin comes with built-in maps for WordPress modules, but we still need to tell Webpack how it should handle any references to WooCommerce packages (which we’re expecting to exist in our environment). Your project may not use all of the WooCommerce packages listed above, but we’re including them here as a best practice. You can read more about how the dependency extraction plugin works in its documentation here.

Finally we’ll combine the default configuration from @wordpress/scripts with the additional items we declared above and declare the export for our file so that Webpack can read it properly:

module.exports = {
    ...defaultConfig,
    plugins: [
        ...defaultConfig.plugins.filter(
            ( plugin ) =>
                plugin.constructor.name !== 'DependencyExtractionWebpackPlugin'
        ),
        new DependencyExtractionWebpackPlugin( {
            injectPolyfill: true,
            requestToExternal,
            requestToHandle,
        } ),
    ],
};

Note: the ...defaultConfig above may look a little confusing if you haven’t worked much with modern JavaScript applications. It’s called a spread operator, and it lets you expand object literals in place. In the first instance, we’re copying the default configuration into our project’s export object. In the second instance, we’re using the spread operator to replace the default DependencyExtractionWebpackPlugin settings with the custom settings we defined above.

Workflow Tip:

There is now a dedicated Dependency Extraction Webpack Plugin for WooCommerce that handles much of the additional configuration we just covered. You can read more about what it does in this blog post or in its README file on GitHub.

Create a src directory to organize JavaScript files

Next we’ll create a directory to hold the modular JavaScript that we’ll be writing. The default WordPress Webpack configuration is set up to look for a file at src/index.js as the primary entry point for the rest of our code, so let’s go ahead and create both of those right now. While we’re at it, we’ll also go ahead and create an SCSS file to hold our project’s style rules.

> mkdir src && touch src/index.js && touch src/index.scss

Now that we have some source files to work with, let’s add some content to them:

// src/index.scss

body {
    background-color: green;
}

and…

// src/index.js

import './index.scss';
console.log( 'hello world' );

You don’t have to make the background green, of course. We just wanted to add a bit of content to illustrate what happens to the CSS rules in the file when we run our build script. Notice also that we’re importing the SCSS file directly into our JavaScript file. The WordPress Webpack configuration is set up to recognize imported SCSS files and will run them through the correct pre-processor.

> npm run build

Now, if we take a look at the files in our build/ folder, we’ll see that they now contain the processed content from our src/ files. You may have noticed that, in addition to the JavaScript and SCSS files in our build/ folder, we also have a file called index.asset.php. This file contains an array of script handles and respective versions for the other WordPress scripts that our project relies on. This will come in handy in our next step.

Note: In our example, the trivial differences between our two sets of files may make the processing seem rather superfluous at the moment; but as our project grows into many modular files that contain specialized syntax (like the JSX that React uses), the benefit of this build system will really become apparent. The build process transforms the modern JavaScript we write into minified code that browsers can interpret correctly.

Register and enqueue the transpiled assets

Now that we have transpiled and minified our JavaScript (and CSS) assets, let’s tell WordPress to load them up for us. We’ll open up our extension’s main PHP file and add an action to the admin_enqueue_scripts hook for this.

/**
 * Register the JS.
 */
function add_extension_register_script() {

    if ( ! class_exists( 'Automattic\WooCommerce\Admin\Loader' ) || ! \Automattic\WooCommerce\Admin\Loader::is_admin_page() ) {
        return;
    }

    $script_path       = '/build/index.js';
    $script_asset_path = dirname( __FILE__ ) . '/build/index.asset.php';
    $script_asset      = file_exists( $script_asset_path )
        ? require( $script_asset_path )
        : array( 'dependencies' => array(), 'version' => filemtime( $script_path ) );
    $script_url = plugins_url( $script_path, __FILE__ );

    wp_register_script(
        'test-extension',
        $script_url,
        $script_asset['dependencies'],
        $script_asset['version'],
        true
    );

    wp_register_style(
        'test-extension',
        plugins_url( '/build/index.css', __FILE__ ),
        // Add any dependencies styles may have, such as wp-components.
        array(),
        filemtime( dirname( __FILE__ ) . '/build/index.css' )
    );

    wp_enqueue_script( 'test-extension' );
    wp_enqueue_style( 'test-extension' );
}

add_action( 'admin_enqueue_scripts', 'add_extension_register_script' );

Let’s break this code down a bit:

if ( ! class_exists( 'Automattic\WooCommerce\Admin\Loader' ) || ! \Automattic\WooCommerce\Admin\Loader::is_admin_page() ) {
        return;
    }

First, before we load any of our JavaScript, we want to make sure that the page we’re on is a JavaScript-powered WooCommerce Admin page. To check, we can use the Loader class in WooCommerce Admin, which has a helper function called is_admin_page() that checks to see whether or not the current page is registered for WooCommerce Admin’s JavaScript support.

$script_path       = '/build/index.js';
    $script_asset_path = dirname( __FILE__ ) . '/build/index.asset.php';
    $script_asset      = file_exists( $script_asset_path )
        ? require( $script_asset_path )
        : array( 'dependencies' => array(), 'version' => filemtime( $script_path ) );
    $script_url = plugins_url( $script_path, __FILE__ );

    wp_register_script(
        'test-extension',
        $script_url,
        $script_asset['dependencies'],
        $script_asset['version'],
        true
    );

Next, we’re collecting various arguments to supply to the wp_register_script() function. The index.asset.php file that Webpack generated for us is really helpful here. We can import it here and supply its list of script handles when we register our JavaScript. That final true in our registration call tells WordPress that this script should be enqueued at the end of the page body instead of in the <HEAD> tag.

Let’s do the same thing for our style rules that we did for our JavaScript:

wp_register_style(
    'test-extension',
    plugins_url( '/build/index.css', __FILE__ ),
    // Add any dependencies styles may have, such as wp-components.
    array(),
    filemtime( dirname( __FILE__ ) . '/build/index.css' )
);

This is a bit more straightforward than registering our JavaScript, but notice that we’re using the timestamp on the asset file as a stand-in when we don’t have an actual version number. This helps prevent static asset caching issues in browsers.

And finally, we’ll have our function tell WordPress to enqueue the registered script and stylesheet:

wp_enqueue_script( 'test-extension' );
wp_enqueue_style( 'test-extension' );

That’s it! You can now use the src folder to build React-based components and add them to your WooCommerce Admin extension. Building those components is outside the scope of this guide, but you can learn more by taking a look at a few examples.

Workflow Tip

Any changes you make to the files in your src/ folder will need to be recompiled before they show up in your project, but you can use the start script that comes with the @wordpress/scripts to do this for you. We configured this script when we set up the scripts object in our package.json file earlier. From your project root, just run npm start, and the script will build the files, watch the src/ folder for any changes, and recompile when necessary.

By Allen Smith

I'm a dad who loves telling stories, connecting people, and making the world a better place.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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