Building Interfaces with Components

Introduction

WooCommerce uses a combination of PHP and modern JavaScript to create a componentized user interface for merchants that is both performant and extensible. If you have never worked with React or other modern JavaScript tools before, you should take a look at the Getting Started guide for React and work through their Intro to React tutorial to get a better understanding of the concepts involved in building modern user interfaces with React before diving in to this guide.

The examples below are based on code that was generated using the extension generator bundled with WooCommerce Admin. If you’re starting a new extension, the extension generator is the recommended approach. If you’re working with an existing extension, your files may look a bit different.

Whether you’re starting fresh or adding functionality to an existing extension, the core concepts are the same:

  • Use PHP to register and enqueue bundled JavaScript files that contain your components.
  • Use JavaScript to define the structure, state, and behavior of your components.
  • Use actions and filters to provide your component to WooCommerce’s admin interface.

This guide assumes you have a JavaScript build system already set up in your project. Setting up a project to support WooCommerce’s React development workflow is outside the scope of this guide, but if you need guidance for manually adding React support to an existing WooCommerce extension, take a look at this walkthrough on the WooCommerce Developer Blog.


Enqueuing JavaScript and stylesheets

There are different ways to go about enqueing JavaScript and CSS files in WordPress. If you use the WooCommerce Extension Generator, it will scaffold out a registration function for you and hook it to the admin_enqueue_scripts action:

/**
 * 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(
        'my-component-example',
        $script_url,
        $script_asset['dependencies'],
        $script_asset['version'],
        true
    );

    wp_register_style(
        'my-component-example',
        plugins_url( '/build/style.css', __FILE__ ),
        // Add any dependencies styles may have, such as wp-components.
        array(),
        filemtime( dirname( __FILE__ ) . '/build/style.css' )
    );

    wp_enqueue_script( 'my-component-example' );
    wp_enqueue_style( 'my-component-example' );
}

add_action( 'admin_enqueue_scripts', 'add_extension_register_script' );

If you’re using the WooCommerce Extension Generator, your build system will be set up to transpile the JavaScript and SCSS files in your project’s src/ directory into minified static assets in the build/ directory.

Note that the example above also uses the index.asset.php file to provide an array of dependencies when registering this extension’s script. If your build system doesn’t generate this file, you may need to specify these dependencies manually to ensure your extension functions correctly.


Building components that display static data

When using the WooCommerce Extension Generator, your extension will be set up to have you create your components and styles in the src/ directory.

The typical entry-point for processing your extension’s components will be the src/index.js file. Think of this file as containing the top-level of your extension’s component hierarchy.

It’s a good idea to use this file for importing top-level components that your extension relies on and for injecting those components into the admin experience using hooks.

As simple example, we’ll start with the custom page we created in the previous section of this guide and populate that page with a nested component that adds a widget containing a list of recent product reviews.

Let’s start by importing a few dependencies to our main JavaScript file (src/index.js).

/**
 * External dependencies
 */

import { addFilter } from '@wordpress/hooks';
import { Component, Fragment } from '@wordpress/element';

/**
 * WooCommerce dependencies
 */
import {
    H,
    Section,
    Table,
} from '@woocommerce/components';

/**
 * Internal dependencies
 */
// import MyInternalComponent from './my-internal-component';
### Tip: Handling Imports

In all of your JavaScript files, it's best to keep your import statements organized into external dependencies, WooCommerce dependencies, and internal modules from within your project itself.  This helps make your code easier to maintain and debug when issues arise.

Setting up the component hierarchy

As mentioned above, we’re starting with the custom page we registered in the previous section.

const MyExamplePage = () => <h1>My Example Extension</h1>;

addFilter( 'woocommerce_admin_pages_list', 'my-namespace', ( pages ) => {
    pages.push( {
        container: MyExamplePage,
        path: '/example',
        breadcrumbs: [ 'My Example Page' ],
        navArgs: {
            id: 'my-example-page',
        },
    } );

    return pages;
} );

For this example, we’ll expand on the MyExamplePage component’s output with our custom component hierarchy. Let’s start by changing MyExamplePage from a simple functional React component to a full ES6 class. This will help us organize our code a bit better and will make it easier to take advantage of React lifecycle hooks, which we’ll use for populating our component with live data.

class MyExamplePage extends Component {
    render() {
        return (
            <h1>My Example Extension</h1>;
        )
    }
}

We’re extending the Component class from the @wordpress/elements package. This class is a wrapper for React.Component tailored for WordPress environments. The render() function above will return the JSX for our page containing our nested component hierarchy. Let’s add a bit of customization to that output.

class MyExamplePage extends Component {

    render() {

        // Using static data for demonstration.
        const recentReviewData = [
            {
                id: 33,
                product_id: 5,
                rating: 4,
                review: 'Great quality'
            },
            {
                id: 32,
                product_id: 17,
                rating: 2, 
                review: 'Runs small.'
            },
            {
                id: 31,
                product_id: 42,
                rating: 5, 
                review: 'Fantastic!.'
            }
        ];

        const headers = [
            { label: 'Product ID' },
            { label: 'Rating' },
            { label: 'Comments' },
        ]

        const rowData = [
            recentReviewData.map( ( review )) => {
                return [
                    { display: review.product_id, value: review.product_id },
                    { display: review.rating, value: review.rating },
                    { display: review.review, value: false }
                ]
            }
        ];

        return (
            <Fragment>
                <H>My Example Extension</H>;
                <Section component='article'>
                    <p>This is a table of recent reviews</p>
                    <Table
                        caption= "Recent Reviews"
                        rows = { rowData }
                        headers = { headers }
                    />
                </Section>
            </Fragment>
        )
    }
}

The changes we made above assign some values to constants which we use to populate the properties of the Table component in our render() method’s return statement. You can read more about available properties and their expected format in the Table component documentation.

#### Tip: Setting up a data placeholder

Before pulling live data, it can be helpful use some static data to get your component's basic hierarchy and behavior set up.  This is what we've done with `recentReviewData` above.  Once you have everything working, you can swap out your static data with functionality that will fetch data from the WooCommerce REST API.

If you rebuild your extension’s JavaScript files and browse to your custom page, you should the content has been updated with the new components we created.


Querying live data to display in components

Once we know our component is displaying everything correctly, we can swap out our static data with functionality that queries the WooCommerce REST API for data when our component loads. To do that, we’ll need to modify our MyExamplePage class a bit. First, we’ll override the default constructor() method to set up some basic state for our component hierarchy.

constructor() {
    super();
    this.state = {
        recentReviewData: []
    }
}

Next, we’ll want to create a method to asynchronously request recent review records from the WooCommerce REST API. Let’s call that getRecentReviewData().

async getRecentReviewData() {

    let recentReviews;
    const reviewsUrl = '/wc/v3/reviews'

    await wp.apiFetch({
        path: reviewsUrl,
        parse: false
    }).then( async (response) => {
        await response.json().then( data => {
            recentReviews = response[data];
        })
    });
    return recentReviews
}

The function above makes use of WordPress’ apiFetch function, which is available in the window and namespaced behind the wp object. If you’re curious about the async and await keywords in the example, you can read more about them in this article about asynchronous functions.

Now that we have a method for requesting data from the WooCommerce REST API, we just need to hook it to our component so that it runs when the component is loaded. To do this, we’ll override React’s componentDidMount() lifecycle method for our component.

async componentDidMount() {
    this.setState({ recentReviewData: await this.getRecentReviewData() });
}

This will run our REST API request when the component has been inserted into the DOM and will use the returned data to update the state of our component. All that we need to do now is update our component’s render() method to pull its data from our component’s state instead of using static data.

const recentReviewData = this.state.recentReviewData;

Handling events in components

We can handle events in our components similar to how we handled the REST API request. We simply create a method to handle the event and then attach that method to the event handler for the appropriate element in our component. As a simple example, if we wanted to add a click event handler for the H element on our top-level component, we’d need to do a few things:

  1. Create the event handler method
  2. Assign the function to the event handler on the element
  3. Bind the value of this so that callbacks work properly

Create the event handler method

handleHeadingClick(e) {
    e.preventDefault();
    alert('You clicked the header!');
}

Assign the function to the event handler

return (
            <Fragment>
                <H onClick={this.handleHeadingClick}>My Example Extension</H>;
                <Section component='article'>
                    <p>This is a table of recent reviews</p>
                    <Table
                        caption= "Recent Reviews"
                        rows = { rowData }
                        headers = { headers }
                    />
                </Section>
            </Fragment>
        )

Bind the value of this for callback handling

Because of how functions work in JavaScript, the value of this can often be tricky to keep track of. There are a few different ways to ensure your callbacks are called within the correct context. You can read more about those approaches in the React docs. The way we’ll choose for our example is to bind the callbacks in the class constructor.

constructor() {
    super();
    this.state = {
        recentReviewData: []
    }

    // Bind event handlers
    this.handleHeadingClick = this.handleHeadingClick.bind(this);
}

Additional Examples

The examples in this guide are only meant for demonstration. If you’d like to see more examples, take a look at the sample extensions available in the WooCommerce Admin repository. You can also read more about components in the WooCommerce Admin developer documentation and the WordPress Components reference.